diff --git a/client/src/components/Home.tsx b/client/src/components/Home.tsx index c56666b..aeccebd 100644 --- a/client/src/components/Home.tsx +++ b/client/src/components/Home.tsx @@ -5,78 +5,101 @@ import { getPow } from "../utils/mine"; import { Event } from "nostr-tools"; import { subGlobalFeed } from "../utils/subscriptions"; import { uniqBy } from "../utils/utils"; -import PWAInstallPopup from "./Modals/PWACheckModal"; +// import PWAInstallPopup from "./Modals/PWACheckModal"; // Removed as it's not being used const Home = () => { + // State declarations const [events, setEvents] = useState([]); - const [filterDifficulty, setFilterDifficulty] = useState( - localStorage.getItem("filterDifficulty") || "20" - ); - const [inBrowser, setInBrowser] = useState(false); + const [filterDifficulty, setFilterDifficulty] = useState(localStorage.getItem("filterDifficulty") || "20"); + const [sortByPoW, setSortByPoW] = useState(true); + // const [inBrowser, setInBrowser] = useState(false); // Removed as it's not being used + // Function to handle new events const onEvent = (event: Event) => { setEvents((prevEvents) => [...prevEvents, event]); }; useEffect(() => { + // Subscribe to the global feed subGlobalFeed(onEvent); - // If you eventually need a cleanup function, put it here + // Event listener to handle difficulty changes const handleDifficultyChange = (event: any) => { - const customEvent = event as CustomEvent; - const { difficulty, filterDifficulty } = customEvent.detail; + const { filterDifficulty } = event.detail; setFilterDifficulty(filterDifficulty); }; - // if ((window.navigator as any).standalone || window.matchMedia('(display-mode: standalone)').matches) { - // console.log('App is running in standalone mode.'); - // } else { - // console.log('App is running in a browser.'); - // setInBrowser(true) - // } - + // Attach event listener window.addEventListener("difficultyChanged", handleDifficultyChange); + // Cleanup listener on component unmount return () => { window.removeEventListener("difficultyChanged", handleDifficultyChange); }; }, []); + // Get unique events based on id const uniqEvents = events.length > 0 ? uniqBy(events, "id") : []; - const filteredAndSortedEvents = uniqEvents - .filter( - (event) => - getPow(event.id) > Number(filterDifficulty) && - event.kind === 1 && - !event.tags.some((tag) => tag[0] === "e") + // Filter and sort events + const filteredEvents = uniqEvents + .filter((event) => + getPow(event.id) > Number(filterDifficulty) && + event.kind === 1 && + !event.tags.some((tag) => tag[0] === "e") ) - .sort((a, b) => (b.created_at as any) - (a.created_at as any)); + const toggleSort = () => { + setSortByPoW(prev => !prev); + }; + + // Events sorted by time + const eventsSortedByTime = [...filteredEvents].sort((a, b) => b.created_at - a.created_at); + + // Events sorted by PoW (assuming `getPow` returns a numerical representation of the PoW) + const eventsSortedByPow = [...filteredEvents].sort((a, b) => getPow(b.id) - getPow(a.id)); + + const displayedEvents = sortByPoW ? eventsSortedByPow : eventsSortedByTime; + + // Get metadata for an event const getMetadataEvent = (event: Event) => { - const metadataEvent = uniqEvents.find( - (e) => e.pubkey === event.pubkey && e.kind === 0 - ); - if (metadataEvent) { - return metadataEvent; - } - return null; + return uniqEvents.find((e) => e.pubkey === event.pubkey && e.kind === 0) || null; }; + // Count replies for an event const countReplies = (event: Event) => { - return uniqEvents.filter((e) => - e.tags.some((tag) => tag[0] === "e" && tag[1] === event.id) - ).length; + return uniqEvents.filter((e) => e.tags.some((tag) => tag[0] === "e" && tag[1] === event.id)).length; }; + // Render the component return (
- {/* {inBrowser && setInBrowser(false)} />} */}
+
+ +
- {filteredAndSortedEvents.map((event) => ( + {displayedEvents.map((event) => ( { return (
- {isExpanded ? finalComment : finalComment.slice(0, 240)} - {finalComment.length > 240 && ( + {isExpanded ? finalComment : finalComment.slice(0, 350)} + {finalComment.length > 350 && (
- {doingWorkProp ? ( -
- - Generating Proof-of-Work... -
- ) : null}
+ {doingWorkProp ? ( +
+ + Generating Proof-of-Work: + iteration {doingWorkProgress} +
+ ) : null}
); diff --git a/client/src/components/Thread/Thread.tsx b/client/src/components/Thread/Thread.tsx index 531d66c..ab0719c 100644 --- a/client/src/components/Thread/Thread.tsx +++ b/client/src/components/Thread/Thread.tsx @@ -6,7 +6,7 @@ import { useEffect } from 'react'; import { uniqBy } from '../../utils/utils'; import { DocumentTextIcon, FolderPlusIcon } from '@heroicons/react/24/outline'; import { generatePrivateKey, getPublicKey, finishEvent } from 'nostr-tools'; -import { minePow } from '../../utils/mine'; +import { getPow } from '../../utils/mine'; import { publish } from '../../utils/relays'; import ThreadPost from './ThreadPost'; import ReplyCard from './ReplyCard'; @@ -23,6 +23,7 @@ const Thread = () => { const [postType, setPostType] = useState(""); const [hasRun, setHasRun] = useState(false); const [preOPEvents, setPreOPEvents] = useState(['']); + const [sortByPoW, setSortByPoW] = useState(false); // Define your callback function for subGlobalFeed const onEvent = (event: Event, relay: string) => { @@ -78,6 +79,17 @@ const Thread = () => { ) .sort((a, b) => (b.created_at as any) - (a.created_at as any)); + const toggleSort = () => { + setSortByPoW(prev => !prev); + }; + + const eventsSortedByTime = [...uniqEvents].slice(1).sort((a, b) => a.created_at - b.created_at); + + // Events sorted by PoW (assuming `getPow` returns a numerical representation of the PoW) + const eventsSortedByPow = [...uniqEvents].slice(1).sort((a, b) => getPow(b.id) - getPow(a.id)); + + const displayedEvents = sortByPoW ? eventsSortedByPow : eventsSortedByTime; + if (!uniqEvents[0]) { return ( <> @@ -132,12 +144,30 @@ const Thread = () => {
+
+ +
{/* This is the white line separator */} - {uniqEvents - .slice(1) - .filter(event => event.kind === 1) - .sort((a, b) => a.created_at - b.created_at).map((event, index) => ( + {displayedEvents.map((event, index) => ( ))}
diff --git a/client/src/components/Thread/ThreadPost.tsx b/client/src/components/Thread/ThreadPost.tsx index 27304b2..71087c4 100644 --- a/client/src/components/Thread/ThreadPost.tsx +++ b/client/src/components/Thread/ThreadPost.tsx @@ -21,13 +21,29 @@ const ThreadPost = ({ OPEvent, state, type }: { OPEvent: NostrEvent, state: Bool const [messageFromWorker, setMessageFromWorker] = useState(null); const [doingWorkProp, setDoingWorkProp] = useState(false); + const [doingWorkProgress, setDoingWorkProgress] = useState(0); + // Initialize the worker outside of any effects - const worker = useMemo(() => new Worker(new URL('../../powWorker', import.meta.url)), []); + const numCores = navigator.hardwareConcurrency || 4; + + const workers = useMemo( + () => Array(numCores).fill(null).map(() => new Worker(new URL("../../powWorker", import.meta.url))), + [] + ); useEffect(() => { - worker.onmessage = (event) => { - setMessageFromWorker(event.data); - }; + workers.forEach((worker) => { + worker.onmessage = (event) => { + if (event.data.status === 'progress') { + console.log(`Worker progress: Checked ${event.data.currentNonce} nonces.`); + setDoingWorkProgress(event.data.currentNonce); + } else if (event.data.found) { + setMessageFromWorker(event.data.event); + // Terminate all workers once a solution is found + workers.forEach(w => w.terminate()); + } + }; + }); const handleDifficultyChange = (event: Event) => { const customEvent = event as CustomEvent; @@ -45,31 +61,36 @@ const ThreadPost = ({ OPEvent, state, type }: { OPEvent: NostrEvent, state: Bool event.preventDefault(); let id = decodeResult.data as string - let tags = []; - let modifiedComment = comment + " " + file; - if (type === 'r') { - tags.push(["e", id as string]) - tags.push(["p", OPEvent.pubkey]) - } else if (type === 'q') { - tags.push(["q", id as string]) - tags.push(["p", OPEvent.pubkey]) - modifiedComment += ' nostr:' + nip19.noteEncode(id); - } + workers.forEach((worker, index) => { + let tags = []; + let modifiedComment = comment + " " + file; + if (type === 'r') { + tags.push(["e", id as string]) + tags.push(["p", OPEvent.pubkey]) + } else if (type === 'q') { + tags.push(["q", id as string]) + tags.push(["p", OPEvent.pubkey]) + modifiedComment += ' nostr:' + nip19.noteEncode(id); + } - try { - worker.postMessage({ - unsigned: { - kind: 1, - tags, - content: modifiedComment, - created_at: Math.floor(Date.now() / 1000), - pubkey: getPublicKey(sk), - }, difficulty - }); + try { + worker.postMessage({ + unsigned: { + kind: 1, + tags, + content: modifiedComment, + created_at: Math.floor(Date.now() / 1000), + pubkey: getPublicKey(sk), + }, + difficulty, + nonceStart: index, // Each worker starts from its index + nonceStep: numCores // Each worker increments by the total number of workers + }); - } catch (error) { - setComment(comment + " " + error); - } + } catch (error) { + setComment(comment + " " + error); + } + }); }; useEffect(() => { @@ -83,10 +104,6 @@ const ThreadPost = ({ OPEvent, state, type }: { OPEvent: NostrEvent, state: Bool setFile(""); setSk(generatePrivateKey()) setMessageFromWorker(null); - - return () => { - worker.terminate(); - }; } catch (error) { setComment(error + ' ' + comment); } @@ -191,15 +208,16 @@ const ThreadPost = ({ OPEvent, state, type }: { OPEvent: NostrEvent, state: Bool Submit
- {doingWorkProp ? ( -
- - Generating Proof-of-Work... -
- ) : null} + {doingWorkProp ? ( +
+ + Generating Proof-of-Work: + iteration {doingWorkProgress} +
+ ) : null} )} ); diff --git a/client/src/powWorker.ts b/client/src/powWorker.ts index 54c7169..c13bdf3 100644 --- a/client/src/powWorker.ts +++ b/client/src/powWorker.ts @@ -7,9 +7,9 @@ const ctx: Worker = self as any; ctx.addEventListener('message', (event) => { console.log("Received message in worker:", event.data); - const { unsigned, difficulty } = event.data; + const { unsigned, difficulty, nonceStart, nonceStep } = event.data; - const result = minePow(unsigned, difficulty); + const result = minePow(unsigned, difficulty, nonceStart, nonceStep); console.log("Mining result:", result); // Post the mined event back to the main thread @@ -43,32 +43,32 @@ export function getPow(hex: string): number { * * Adapted from Snort: https://git.v0l.io/Kieran/snort/src/commit/4df6c19248184218c4c03728d61e94dae5f2d90c/packages/system/src/pow-util.ts#L14-L36 */ -export function minePow(unsigned: UnsignedEvent, difficulty: number): Omit, 'sig'> { - let count = 0 +export function minePow(unsigned: UnsignedEvent, difficulty: number, nonceStart: number, nonceStep: number): { found: boolean, event?: Omit, 'sig'> } { + let nonce = nonceStart; const event = unsigned as Omit, 'sig'> - const tag = ['nonce', count.toString(), difficulty.toString()] + const tag = ['nonce', nonce.toString(), difficulty.toString()] - event.tags.push(tag) + event.tags.push(tag); + // We use a while loop that might run indefinitely until a solution is found. + // Consider adding a breaking condition if you want to limit the number of nonces each worker checks. while (true) { - const now = Math.floor(new Date().getTime() / 1000) + tag[1] = (nonce).toString(); - if (now !== event.created_at) { - count = 0 - event.created_at = now - } - - tag[1] = (++count).toString() - - event.id = getEventHash(event) + event.id = getEventHash(event); if (getPow(event.id) >= difficulty) { - break + return { found: true, event: event }; + } + + nonce += nonceStep; + + if (nonce % (nonceStep * 10000) === 0) { + ctx.postMessage({ status: 'progress', currentNonce: nonce }); } } - - return event + return { found: false }; } export default ctx;