good enough

This commit is contained in:
smolgrrr 2023-11-08 22:23:57 +11:00
parent 9c540eda8c
commit 940a891943
9 changed files with 57 additions and 336 deletions

View File

@ -5,7 +5,7 @@ import {
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import { XCircleIcon } from "@heroicons/react/24/solid"; import { XCircleIcon } from "@heroicons/react/24/solid";
import { useState, useEffect, useMemo } from "react"; import { useState, useEffect, useMemo } from "react";
import { generatePrivateKey, getPublicKey, finishEvent, UnsignedEvent } from "nostr-tools"; import { generatePrivateKey, getPublicKey, finishEvent, UnsignedEvent, Event as NostrEvent, nip19 } from "nostr-tools";
import { publish } from "../../utils/relays"; import { publish } from "../../utils/relays";
import { renderMedia, attachFile } from "../../utils/FileUpload"; import { renderMedia, attachFile } from "../../utils/FileUpload";
@ -40,7 +40,20 @@ const useWorkers = (numCores: number, unsigned: UnsignedEvent, difficulty: strin
return { startWork, messageFromWorker, doingWorkProgress }; return { startWork, messageFromWorker, doingWorkProgress };
}; };
const NewNoteCard: React.FC = () => { interface FormProps {
refEvent?: NostrEvent;
tagType?: 'Reply' | 'Quote' | '';
}
const tagMapping = {
'Reply': ['e', 'p'],
'Quote': ['q', 'p']
};
const NewNoteCard = ({
refEvent,
tagType
}: FormProps) => {
const [comment, setComment] = useState(""); const [comment, setComment] = useState("");
const [file, setFile] = useState(""); const [file, setFile] = useState("");
const [sk, setSk] = useState(generatePrivateKey()); const [sk, setSk] = useState(generatePrivateKey());
@ -65,6 +78,17 @@ const NewNoteCard: React.FC = () => {
useEffect(() => { useEffect(() => {
if (refEvent && tagType && unsigned.tags.length === 0) {
const tags = tagMapping[tagType];
if (tags) {
tags.forEach(tag => unsigned.tags.push([tag, refEvent[tag === 'p' ? 'pubkey' : 'id']]));
}
if (tagType === 'Quote') {
setComment(comment + ' nostr:' + nip19.noteEncode(refEvent.id));
}
}
const handleDifficultyChange = (event: Event) => { const handleDifficultyChange = (event: Event) => {
const customEvent = event as CustomEvent; const customEvent = event as CustomEvent;
const { difficulty } = customEvent.detail; const { difficulty } = customEvent.detail;
@ -79,15 +103,12 @@ const NewNoteCard: React.FC = () => {
}, []); }, []);
useEffect(() => { useEffect(() => {
setUnsigned( setUnsigned(prevUnsigned => ({
{ ...prevUnsigned,
kind: 1, content: `${comment} ${file}`,
tags: [], created_at: Math.floor(Date.now() / 1000),
content: comment + " " + file, pubkey: getPublicKey(sk),
created_at: Math.floor(Date.now() / 1000), }));
pubkey: getPublicKey(sk),
}
);
}, [comment, file]); }, [comment, file]);
useEffect(() => { useEffect(() => {
@ -100,16 +121,12 @@ const NewNoteCard: React.FC = () => {
setComment(""); setComment("");
setFile(""); setFile("");
setSk(generatePrivateKey()); setSk(generatePrivateKey());
setUnsigned( setUnsigned(prevUnsigned => ({
{ ...prevUnsigned,
kind: 1, content: '',
tags: [], created_at: Math.floor(Date.now() / 1000),
content: "", pubkey: getPublicKey(sk),
created_at: Math.floor(Date.now() / 1000), }));
pubkey: getPublicKey(sk),
}
);
} catch (error) { } catch (error) {
setComment(error + " " + comment); setComment(error + " " + comment);
} }

View File

@ -1,226 +0,0 @@
import { useParams } from 'react-router-dom';
import { useState, useMemo, useEffect } from "react";
import { ArrowUpTrayIcon, CpuChipIcon, ArrowPathIcon } from '@heroicons/react/24/outline';
import { XCircleIcon } from '@heroicons/react/24/solid';
import { generatePrivateKey, getPublicKey, finishEvent, Event as NostrEvent } from 'nostr-tools';
import { publish } from '../../utils/relays';
import FileUpload from '../../utils/FileUpload';
import { nip19 } from 'nostr-tools';
import { renderMedia } from '../../utils/FileUpload';
const ThreadPost = ({ OPEvent, state, type }: { OPEvent: NostrEvent, state: Boolean, type: String }) => {
const { id } = useParams();
const [comment, setComment] = useState("");
const [file, setFile] = useState("");
const [difficulty, setDifficulty] = useState(localStorage.getItem('difficulty') || '21');
const [uploadingFile, setUploadingFile] = useState(false);
let decodeResult = nip19.decode(id as string);
const [sk, setSk] = useState(generatePrivateKey());
const [messageFromWorker, setMessageFromWorker] = useState(null);
const [doingWorkProp, setDoingWorkProp] = useState(false);
const [doingWorkProgress, setDoingWorkProgress] = useState(0);
// Initialize the worker outside of any effects
const numCores = navigator.hardwareConcurrency || 4;
const workers = useMemo(
() => Array(numCores).fill(null).map(() => new Worker(new URL("../../powWorker", import.meta.url))),
[]
);
useEffect(() => {
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;
setDifficulty(customEvent.detail);
};
window.addEventListener('difficultyChanged', handleDifficultyChange);
return () => {
window.removeEventListener('difficultyChanged', handleDifficultyChange);
};
}, []);
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
let id = decodeResult.data as string
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,
nonceStart: index, // Each worker starts from its index
nonceStep: numCores // Each worker increments by the total number of workers
});
} catch (error) {
setComment(comment + " " + error);
}
});
};
useEffect(() => {
setDoingWorkProp(false)
if (messageFromWorker) {
try {
const signedEvent = finishEvent(messageFromWorker, sk);
publish(signedEvent);
setComment("");
setFile("");
setSk(generatePrivateKey())
setMessageFromWorker(null);
} catch (error) {
setComment(error + ' ' + comment);
}
}
}, [messageFromWorker]);
async function attachFile(file_input: File | null) {
setUploadingFile(true); // start loading
try {
if (file_input) {
const rx = await FileUpload(file_input);
setUploadingFile(false); // stop loading
if (rx.url) {
setFile(rx.url);
} else if (rx?.error) {
setFile(rx.error);
}
}
} catch (error: unknown) {
setUploadingFile(false); // stop loading
if (error instanceof Error) {
setFile(error?.message);
}
}
}
return (
<>
{state && (
<form
name="post"
method="post"
encType="multipart/form-data"
className=""
onSubmit={(event) => {
handleSubmit(event);
setDoingWorkProp(true);
}}
>
<input type="hidden" name="MAX_FILE_SIZE" defaultValue={4194304} />
<div id="togglePostFormLink" className="text-lg font-semibold">
{type === 'r' ? <span>Reply To Post</span> : <span>Quote Post</span>}
</div>
<div className="px-4 pt-4 flex flex-col bg-neutral-900 border border-neutral-800 rounded-lg">
<div>
<textarea
name="com"
wrap="soft"
className="shadow-lg w-full px-4 py-3 h-28 rounded-md outline-none focus:outline-none bg-neutral-800 border border-neutral-700 text-white placeholder:text-neutral-500"
placeholder='Shitpost here...'
value={comment}
onChange={(e) => setComment(e.target.value)}
/>
</div>
<div className="relative">
{file !== "" && (
<button onClick={() => setFile("")}>
<XCircleIcon className="h-10 w-10 absolute shadow z-100 text-blue-500" />
</button>
)}
{renderMedia(file)}
</div>
<div className="h-14 flex items-center justify-between">
<div className="inline-flex items-center gap-2 bg-neutral-800 px-1.5 py-1 rounded-lg">
<div className="inline-flex items-center gap-1.5 text-neutral-300">
<CpuChipIcon className="h-4 w-4" />
</div>
<p className="text-xs font-medium text-neutral-400">
{difficulty} PoW
</p>
</div>
<div>
<div className="flex items-center gap-4">
<div className="flex items-center">
<ArrowUpTrayIcon
className="h-4 w-4 text-neutral-400 cursor-pointer"
onClick={() => document.getElementById("file_input")?.click()}
/>
<input
type="file"
name="file_input"
id="file_input"
style={{ display: "none" }}
onChange={(e) => {
const file_input = e.target.files?.[0];
if (file_input) {
attachFile(file_input);
}
}}
/>
{uploadingFile ? (
<div className="flex animate-spin text-sm text-gray-300">
<ArrowPathIcon className="h-4 w-4 ml-auto" />
</div>
) : null}
</div>
<button
type="submit"
className="h-9 inline-flex items-center justify-center px-4 bg-blue-500 hover:bg-blue-600 rounded-lg text-white font-medium text-sm"
>
Submit
</button>
</div>
</div>
</div>
</div>
{doingWorkProp ? (
<div className="flex animate-pulse text-sm text-gray-300">
<CpuChipIcon className="h-4 w-4 ml-auto" />
<span>Generating Proof-of-Work:</span>
<span>iteration {doingWorkProgress}</span>
</div>
) : null}
</form>)}
</>
);
};
export default ThreadPost;

View File

@ -1,6 +1,6 @@
import { useEffect, useState, useCallback } from "react"; import { useEffect, useState, useCallback } from "react";
import PostCard from "./Modals/Card"; import PostCard from "./Modals/Card";
import { uniqBy } from "../utils/utils"; // Assume getPow is a correct import now import { uniqBy } from "../utils/otherUtils"; // Assume getPow is a correct import now
import { subGlobalFeed } from "../utils/subscriptions"; import { subGlobalFeed } from "../utils/subscriptions";
import { verifyPow } from "../utils/mine"; import { verifyPow } from "../utils/mine";
import { Event } from "nostr-tools"; import { Event } from "nostr-tools";

View File

@ -2,12 +2,12 @@ import CardContainer from "./CardContainer";
import { FolderIcon, CpuChipIcon } from "@heroicons/react/24/outline"; import { FolderIcon, CpuChipIcon } from "@heroicons/react/24/outline";
import { parseContent } from "../../utils/content"; import { parseContent } from "../../utils/content";
import { Event, nip19 } from "nostr-tools"; import { Event, nip19 } from "nostr-tools";
import { getMetadata } from "../../utils/utils"; import { getMetadata } from "../../utils/otherUtils";
import ContentPreview from "./CardModals/TextModal"; import ContentPreview from "./CardModals/TextModal";
import { renderMedia } from "../../utils/FileUpload"; import { renderMedia } from "../../utils/FileUpload";
import { getIconFromHash } from "../../utils/deterministicProfileIcon"; import { getIconFromHash } from "../../utils/deterministicProfileIcon";
import { verifyPow } from "../../utils/mine"; import { verifyPow } from "../../utils/mine";
import { uniqBy } from "../../utils/utils"; import { uniqBy } from "../../utils/otherUtils";
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
const timeUnits = [ const timeUnits = [

View File

@ -1,6 +1,6 @@
import { parseContent } from "../../../utils/content"; import { parseContent } from "../../../utils/content";
import { Event } from "nostr-tools"; import { Event } from "nostr-tools";
import { getMetadata, uniqBy } from "../../../utils/utils"; import { getMetadata, uniqBy } from "../../../utils/otherUtils";
import ContentPreview from "./TextModal"; import ContentPreview from "./TextModal";
import { renderMedia } from "../../../utils/FileUpload"; import { renderMedia } from "../../../utils/FileUpload";

View File

@ -3,22 +3,21 @@ import { useState } from "react";
import { Event, nip19 } from "nostr-tools" import { Event, nip19 } from "nostr-tools"
import { subNote, subNotesOnce } from '../utils/subscriptions'; import { subNote, subNotesOnce } from '../utils/subscriptions';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { uniqBy } from '../utils/utils'; import { uniqBy } from '../utils/otherUtils';
import { DocumentTextIcon, FolderPlusIcon } from '@heroicons/react/24/outline'; import { DocumentTextIcon, FolderPlusIcon } from '@heroicons/react/24/outline';
import { getPow } from '../utils/mine'; import { getPow } from '../utils/mine';
import ThreadPost from './Forms/ThreadPost';
import PostCard from './Modals/Card'; import PostCard from './Modals/Card';
import Placeholder from './Modals/Placeholder'; import Placeholder from './Modals/Placeholder';
import NewNoteCard from './Forms/PostFormCard';
type PostType = "" | "Reply" | "Quote" | undefined;
const difficulty = 20
const Thread = () => { const Thread = () => {
const { id } = useParams(); const { id } = useParams();
const [events, setEvents] = useState<Event[]>([]); // Initialize state const [events, setEvents] = useState<Event[]>([]); // Initialize state
const [OPEvent, setOPEvent] = useState<Event>() const [OPEvent, setOPEvent] = useState<Event>()
const [showForm, setShowForm] = useState(false); const [showForm, setShowForm] = useState(false);
const [postType, setPostType] = useState(""); const [postType, setPostType] = useState<PostType>("");
const [hasRun, setHasRun] = useState(false); const [hasRun, setHasRun] = useState(false);
const [preOPEvents, setPreOPEvents] = useState(['']); const [preOPEvents, setPreOPEvents] = useState(['']);
const [sortByTime, setSortByTime] = useState(true); const [sortByTime, setSortByTime] = useState(true);
@ -48,7 +47,12 @@ const Thread = () => {
useEffect(() => { useEffect(() => {
if (!hasRun && events.length > 0) { if (!hasRun && events.length > 0) {
let OPEvent = events.find(e => e.id === hexID); let OPEvent = uniqEvents[0];
setOPEvent(OPEvent);
if (OPEvent && OPEvent.id !== hexID) {
OPEvent = events.find(e => e.id === hexID) as Event;
}
if (OPEvent) { if (OPEvent) {
setOPEvent(OPEvent); setOPEvent(OPEvent);
@ -119,7 +123,7 @@ const Thread = () => {
className="h-5 w-5 text-gray-200" className="h-5 w-5 text-gray-200"
onClick={() => { onClick={() => {
setShowForm(prevShowForm => !prevShowForm); setShowForm(prevShowForm => !prevShowForm);
setPostType('r'); setPostType('Reply');
}} }}
/> />
@ -127,13 +131,13 @@ const Thread = () => {
className="h-5 w-5 text-gray-200" className="h-5 w-5 text-gray-200"
onClick={() => { onClick={() => {
setShowForm(prevShowForm => !prevShowForm); setShowForm(prevShowForm => !prevShowForm);
setPostType('q'); setPostType('Quote');
}} }}
/> />
</div> </div>
<div className="w-full px-4 sm:px-0 sm:max-w-xl mx-auto my-2"> {(showForm && postType) && <div className="w-full px-4 sm:px-0 sm:max-w-xl mx-auto my-2">
<ThreadPost OPEvent={uniqEvents[0]} state={showForm} type={postType} /> <NewNoteCard refEvent={uniqEvents[0]} tagType={postType}/>
</div> </div>}
<div className="flex items-center justify-center w-full py-4"> <div className="flex items-center justify-center w-full py-4">
<label htmlFor="toggleB" className="flex items-center cursor-pointer"> <label htmlFor="toggleB" className="flex items-center cursor-pointer">
<div className="relative"> <div className="relative">

View File

@ -1,32 +0,0 @@
import {
type Event as NostrEvent,
generatePrivateKey,
getEventHash,
getPublicKey,
signEvent,
} from "nostr-tools";
export const handleThreadSubmit = async (comment: string, tags: []) => {
if (!comment) {
alert("no message provided");
return;
}
const newEvent: NostrEvent = {
id: 'null',
content: comment,
kind: 1,
tags,
created_at: Math.floor(Date.now() / 1000),
pubkey: 'null',
sig: 'null',
};
let sk = generatePrivateKey();
newEvent.pubkey = getPublicKey(sk);
newEvent.id = getEventHash(newEvent);
newEvent.sig = signEvent(newEvent, sk);
return newEvent
};

View File

@ -1,42 +0,0 @@
import { useMemo, useEffect } from 'react';
interface Dispatch {
(arg: { type: string, payload?: any }): void
}
const useWorkers = (dispatch: Dispatch) => {
const numCores = navigator.hardwareConcurrency || 4;
const workers = useMemo(
() => Array(numCores).fill(null).map(() => new Worker(new URL("../../powWorker", import.meta.url))),
[]
);
useEffect(() => {
workers.forEach((worker) => {
worker.onmessage = (event) => {
if (event.data.status === 'progress') {
dispatch({ type: 'SET_WORK_PROGRESS', payload: event.data.currentNonce });
} else if (event.data.found) {
dispatch({ type: 'SET_MESSAGE_FROM_WORKER', payload: event.data.event });
workers.forEach(w => w.terminate());
}
};
});
const handleDifficultyChange = (event: Event) => {
const customEvent = event as CustomEvent;
dispatch({ type: 'SET_DIFFICULTY', payload: customEvent.detail });
};
window.addEventListener('difficultyChanged', handleDifficultyChange);
return () => {
window.removeEventListener('difficultyChanged', handleDifficultyChange);
};
}, []);
return workers;
};
export default useWorkers;