mirror of
https://github.com/aljazceru/landscape-template.git
synced 2026-01-31 12:14:30 +01:00
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:
@@ -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")
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user