refactor: re-wrote the comments.worker code to a custom hook that is cleaner, ending loading state after new event is fetched

This commit is contained in:
MTG2000
2022-07-29 10:52:44 +03:00
parent 7164daa7b9
commit 0004c4d349
4 changed files with 276 additions and 15 deletions

View File

@@ -27,11 +27,13 @@ const signEvent = async (req, res) => {
return res.status(400).send("Signature not valid")
// Extract type & id
const rTag = event.tags.find(tag => tag[0] === 'r');
const [host, type, refId] = rTag[1].split(' ');
if (host !== 'boltfun') res.status(400).send("This event wasn't signed by bolt.fun");
if (host !== 'boltfun') return res.status(400).send("This event wasn't signed by bolt.fun");
if (type === 'Story_comment') {
@@ -48,6 +50,7 @@ const signEvent = async (req, res) => {
}))?.id;
}
// Insert comment in database
await prisma.postComment.create({
data: {
@@ -59,10 +62,14 @@ const signEvent = async (req, res) => {
},
})
}
return res
.status(200)
.end()
} catch (error) {
console.log(error);
res.status(500).send("Unexpected error happened, please try again")

View File

@@ -7,6 +7,7 @@ import { useAppSelector } from "src/utils/hooks";
import * as CommentsWorker from './comments.worker'
import { Post_Type } from 'src/graphql'
import useComments from './useComments'
// const createWorker = createWorkerFactory(() => import('./comments.worker'));
@@ -21,24 +22,25 @@ export default function CommentsSection({ type, id }: Props) {
// const worker = useWorker(createWorker);
// const commentsTree = useMemo(() => convertCommentsToTree(comments), [comments])
const [commentsTree, setCommentsTree] = useState<Comment[]>([])
// const [commentsTree, setCommentsTree] = useState<Comment[]>([])
const user = useAppSelector(state => state.user.me)
const filter = useMemo(() => `boltfun ${type}_comment ${id}` + (process.env.NODE_ENV === 'development' ? ' dev' : ""), [id, type])
// const filter = useMemo(() => `boltfun ${type}_comment ${id}` + (process.env.NODE_ENV === 'development' ? ' dev' : ""), [id, type])
useEffect(() => {
CommentsWorker.connect();
const unsub = CommentsWorker.sub(filter, (newComments) => {
setCommentsTree(newComments)
})
// useEffect(() => {
// CommentsWorker.connect();
// const unsub = CommentsWorker.sub(filter, (newComments) => {
// setCommentsTree(newComments)
// })
return () => {
unsub();
}
}, [filter]);
// return () => {
// unsub();
// }
// }, [filter]);
const { commentsTree, postComment } = useComments({ type, id })
const handleNewComment = async (content: string, parentId?: string) => {
try {
await CommentsWorker.post({ content, filter, parentId });
await postComment({ content, parentId });
return true;
} catch (error) {
return false
@@ -63,7 +65,7 @@ export default function CommentsSection({ type, id }: Props) {
comment={comment}
isRoot
canReply={!!user}
onReply={content => handleNewComment(content, comment.id.toString())}
onReply={content => handleNewComment(content, comment.nostr_id.toString())}
/>)}
</div>
</div>

View File

@@ -0,0 +1,252 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { relayPool } from 'nostr-tools'
import { Nullable } from 'remirror';
import { CONSTS } from 'src/utils';
import { Comment } from "../types";
import { useDebouncedState } from "@react-hookz/web";
import { Post_Type } from "src/graphql";
const pool = relayPool();
const useComments = (config: {
type: Post_Type,
id: string | number;
}) => {
const commentsEventsTemp = useRef<Record<string, Required<NostrEvent>>>({})
const [commentsEvents, setCommentsEvents] = useDebouncedState<Record<string, Required<NostrEvent>>>({}, 1000)
const pendingResolvers = useRef<Record<string, () => void>>({});
const filter = useMemo(() => `boltfun ${config.type}_comment ${config.id}` + (process.env.NODE_ENV === 'development' ? ' dev' : ""), [config.id, config.type])
const [commentsTree, setCommentsTree] = useState<Comment[]>([])
useEffect(() => {
connect();
let sub = pool.sub({
filter: {
"#r": [filter]
},
cb: async (event: Required<NostrEvent>) => {
//Got a new event
if (!event.id) return;
if (event.id in commentsEventsTemp.current) return;
commentsEventsTemp.current[event.id] = event;
setCommentsEvents({ ...commentsEventsTemp.current })
}
});
return () => {
sub.unsub();
};
}, [filter, setCommentsEvents]);
useEffect(() => {
(async () => {
const newTree = await buildTree(commentsEvents);
setCommentsTree(newTree);
Object.entries(pendingResolvers.current).forEach(([id, resolve]) => {
if (id in commentsEvents) {
delete pendingResolvers.current[id];
resolve();
}
});
})();
}, [commentsEvents])
const postComment = useCallback(async ({ content, parentId }: {
content: string,
parentId?: string
}) => {
const tags = [];
tags.push(['r', filter]);
if (parentId)
tags.push(['e', `${parentId} ${CONSTS.DEFAULT_RELAYS[0]} reply`])
let event: NostrEvent;
try {
event = await signEvent({
kind: 1,
tags,
content,
}) as NostrEvent;
} catch (error) {
alert("Couldn't sign the object successfully...")
return;
}
return new Promise<void>((resolve, reject) => {
let confirmationSent = false;
pool.publish(event, (status: number, relay: string) => {
switch (status) {
case -1:
console.log(`failed to send ${JSON.stringify(event)} to ${relay}`)
break
case 1:
clearTimeout(publishTimeout)
console.log(`event ${event.id?.slice(0, 5)}… published to ${relay}.`)
if (!confirmationSent) {
confirmPublishingEvent(event)
confirmationSent = true;
}
break
}
});
pendingResolvers.current[event.id!] = resolve;
const publishTimeout = setTimeout(() => {
delete pendingResolvers.current[event.id!]
reject("Failed to publish to any relay...");
}, 5000)
})
}, [filter])
return { commentsTree, postComment }
}
export default useComments;
function connect() {
CONSTS.DEFAULT_RELAYS.forEach(url => {
pool.addRelay(url, { read: true, write: true })
})
pool.onNotice((notice: string, relay: any) => {
console.log(`${relay.url} says: ${notice}`)
})
};
function extractParentId(event: NostrEvent): Nullable<string> {
for (const [identifier, value] of event.tags) {
if (identifier === 'e') {
const [eventId, , marker] = value.split(' ');
if (marker === 'reply') return eventId;
}
}
return null;
}
async function signEvent(event: any) {
const res = await fetch(CONSTS.apiEndpoint + '/nostr-sign-event', {
method: "post",
body: JSON.stringify({ event }),
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
});
const data = await res.json()
return data.event;
}
async function confirmPublishingEvent(event: any) {
const res = await fetch(CONSTS.apiEndpoint + '/nostr-confirm-event', {
method: "post",
body: JSON.stringify({ event }),
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
});
const data = await res.json()
return data.event;
}
async function getCommentsExtraData(ids: string[]) {
const res = await fetch(CONSTS.apiEndpoint + '/nostr-events-extra-data', {
method: "post",
body: JSON.stringify({ ids }),
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
});
type EventExtraData = {
id: number
nostr_id: string
votes_count: number
user: {
id: number,
avatar: string,
name: string,
}
}
const data = await res.json() as EventExtraData[];
const map = new Map<string, EventExtraData>()
data.forEach(item => {
map.set(item.nostr_id, item)
});
return map;
}
async function buildTree(events: Record<string, Required<NostrEvent>>) {
// Sort them chronologically from oldest to newest
let sortedEvenets = Object.values(events).sort((a, b) => a.created_at - b.created_at);
// Extract the pubkeys used
const pubkeysSet = new Set<string>();
sortedEvenets.forEach(e => pubkeysSet.add(e.pubkey));
// Make a request to api to get comments extra data
const commentsExtraData = await getCommentsExtraData(Object.keys(events));
let eventsTree: Record<string, Comment> = {}
// If event is a reply, connect it to parent
sortedEvenets.forEach(e => {
const parentId = extractParentId(e);
const extraData = commentsExtraData.get(e.id);
// if no extra data is here then that means this event wasn't done from our platform
if (!extraData) return;
if (parentId) {
eventsTree[parentId]?.replies.push({
id: extraData.id,
nostr_id: e.id,
body: e.content,
created_at: e.created_at * 1000,
pubkey: e.pubkey,
author: extraData.user,
replies: [],
votes_count: extraData.votes_count
});
} else {
eventsTree[e.id] = ({
id: extraData.id,
nostr_id: e.id,
body: e.content,
created_at: e.created_at * 1000,
pubkey: e.pubkey,
author: extraData.user,
replies: [],
votes_count: extraData.votes_count
});
}
})
// Run the censoring service
// (nothing for now -:-)
// Turn the top roots replies into a sorted array
const sortedTree = Object.values(eventsTree).sort((a, b) => b.created_at - a.created_at)
return sortedTree;
}

View File

@@ -13,7 +13,7 @@ export default function PageContentSkeleton() {
</h1>
<HeaderSkeleton />
<div className="flex flex-wrap gap-8">
{Array(3).fill(0).map(i => <Badge key={i} size='sm'>
{Array(3).fill(0).map((_, idx) => <Badge key={idx} size='sm'>
<div className="opacity-0">hidden</div>
</Badge>)}
</div>