From 97b44a55f2ae065131b3f9324e7c7d8d4793ffe9 Mon Sep 17 00:00:00 2001 From: en Date: Tue, 28 Jan 2025 15:35:30 +0100 Subject: [PATCH] feat(reply): publish new reply with ndkevent, fetch kind 1 and 1111 --- src/components/comment/index.tsx | 273 ++++++++++++++----------------- src/hooks/useComments.ts | 16 +- src/pages/blog/loader.ts | 4 + src/pages/mod/loader.ts | 4 + src/types/blog.ts | 2 + src/types/mod.ts | 6 +- 6 files changed, 139 insertions(+), 166 deletions(-) diff --git a/src/components/comment/index.tsx b/src/components/comment/index.tsx index acb7c0f..7190fb1 100644 --- a/src/components/comment/index.tsx +++ b/src/components/comment/index.tsx @@ -1,4 +1,4 @@ -import { NDKEvent } from '@nostr-dev-kit/ndk' +import { NDKEvent, NDKKind, NostrEvent } from '@nostr-dev-kit/ndk' import { Dots, Spinner } from 'components/Spinner' import { ZapPopUp } from 'components/Zap' import { formatDate } from 'date-fns' @@ -10,7 +10,7 @@ import { useReactions } from 'hooks' import { useComments } from 'hooks/useComments' -import { Event, kinds, nip19, UnsignedEvent } from 'nostr-tools' +import { nip19 } from 'nostr-tools' import React, { Dispatch, SetStateAction, @@ -18,16 +18,18 @@ import React, { useMemo, useState } from 'react' -import { Link } from 'react-router-dom' +import { Link, useLoaderData } from 'react-router-dom' import { toast } from 'react-toastify' import { getProfilePageRoute } from 'routes' import { Addressable, + BlogPageLoaderResult, CommentEvent, CommentEventStatus, + ModPageLoaderResult, UserProfile } from 'types/index.ts' -import { abbreviateNumber, hexToNpub, log, LogType, now } from 'utils' +import { abbreviateNumber, hexToNpub, log, LogType } from 'utils' enum SortByEnum { Latest = 'Latest', @@ -50,11 +52,14 @@ type Props = { } export const Comments = ({ addressable, setCommentCount }: Props) => { - const { ndk, publish } = useNDKContext() + const { ndk } = useNDKContext() const { commentEvents, setCommentEvents } = useComments( addressable.author, addressable.aTag ) + const { event } = useLoaderData() as + | ModPageLoaderResult + | BlogPageLoaderResult const [filterOptions, setFilterOptions] = useState({ sort: SortByEnum.Latest, author: AuthorFilterEnum.All_Comments @@ -73,120 +78,73 @@ export const Comments = ({ addressable, setCommentCount }: Props) => { setCommentCount(commentEvents.length) }, [commentEvents, setCommentCount]) - const userState = useAppSelector((state) => state.user) - const handleSubmit = async (content: string): Promise => { if (content === '') return false - let pubkey: string | undefined + // NDKEvent required + if (!event) return false - if (userState.auth && userState.user?.pubkey) { - pubkey = userState.user.pubkey as string - } else { - try { - pubkey = (await window.nostr?.getPublicKey()) as string - } catch (error) { - log(true, LogType.Error, `Could not get pubkey`, error) - } - } + try { + const reply = event.reply() + reply.content = content - if (!pubkey) { - toast.error('Could not get user pubkey') - return false - } - - const unsignedEvent: UnsignedEvent = { - content: content, - pubkey: pubkey, - kind: kinds.ShortTextNote, - created_at: now(), - tags: [ - ['e', addressable.id], - ['a', addressable.aTag], - ['p', addressable.author] - ] - } - - const signedEvent = await window.nostr - ?.signEvent(unsignedEvent) - .then((event) => event as Event) - .catch((err) => { - toast.error('Failed to sign the event!') - log(true, LogType.Error, 'Failed to sign the event!', err) - return null - }) - - if (!signedEvent) return false - - setCommentEvents((prev) => [ - { - ...signedEvent, - status: CommentEventStatus.Publishing - }, - ...prev - ]) - - const ndkEvent = new NDKEvent(ndk, signedEvent) - publish(ndkEvent) - .then((publishedOnRelays) => { - if (publishedOnRelays.length === 0) { - setCommentEvents((prev) => - prev.map((event) => { - if (event.id === signedEvent.id) { - return { - ...event, - status: CommentEventStatus.Failed - } + setCommentEvents((prev) => [ + { + event: new NDKEvent(ndk, reply), + status: CommentEventStatus.Publishing + }, + ...prev + ]) + const relaySet = await reply.publish() + if (relaySet.size) { + setCommentEvents((prev) => + prev.map((ce) => { + if (ce.event.id === reply.id) { + return { + event: ce.event, + status: CommentEventStatus.Published } - - return event - }) - ) - } else { - setCommentEvents((prev) => - prev.map((event) => { - if (event.id === signedEvent.id) { - return { - ...event, - status: CommentEventStatus.Published - } - } - - return event - }) - ) - } - + } + return ce + }) + ) // when an event is successfully published remove the status from it after 15 seconds setTimeout(() => { setCommentEvents((prev) => - prev.map((event) => { - if (event.id === signedEvent.id) { - delete event.status + prev.map((ce) => { + if (ce.event.id === reply.id) { + delete ce.status } - return event + return ce }) ) }, 15000) - }) - .catch((err) => { - console.error('An error occurred in publishing comment', err) + } else { + log(true, LogType.Error, 'Publishing comment failed.') setCommentEvents((prev) => - prev.map((event) => { - if (event.id === signedEvent.id) { + prev.map((ce) => { + if (ce.event.id === reply.id) { return { - ...event, + event: ce.event, status: CommentEventStatus.Failed } } - - return event + return ce }) ) - }) - - return true + } + return false + } catch (error) { + toast.error('An error occurred in publishing comment.') + log( + true, + LogType.Error, + 'An error occurred in publishing comment.', + error + ) + return false + } } const handleDiscoveredClick = () => { @@ -203,14 +161,22 @@ export const Comments = ({ addressable, setCommentCount }: Props) => { let filteredComments = visible if (filterOptions.author === AuthorFilterEnum.Creator_Comments) { filteredComments = filteredComments.filter( - (comment) => comment.pubkey === addressable.author + (comment) => comment.event.pubkey === addressable.author ) } if (filterOptions.sort === SortByEnum.Latest) { - filteredComments.sort((a, b) => b.created_at - a.created_at) + filteredComments.sort((a, b) => + a.event.created_at && b.event.created_at + ? b.event.created_at - a.event.created_at + : 0 + ) } else if (filterOptions.sort === SortByEnum.Oldest) { - filteredComments.sort((a, b) => a.created_at - b.created_at) + filteredComments.sort((a, b) => + a.event.created_at && b.event.created_at + ? a.event.created_at - b.event.created_at + : 0 + ) } return filteredComments @@ -241,8 +207,8 @@ export const Comments = ({ addressable, setCommentCount }: Props) => { setFilterOptions={setFilterOptions} />
- {comments.map((event) => ( - + {comments.map((comment) => ( + ))}
@@ -361,20 +327,19 @@ const Filter = React.memo( ) } ) - -const Comment = (props: CommentEvent) => { - const { findMetadata } = useNDKContext() +interface CommentProps { + comment: CommentEvent +} +const Comment = ({ comment }: CommentProps) => { const [profile, setProfile] = useState() useDidMount(() => { - findMetadata(props.pubkey).then((res) => { - setProfile(res) - }) + comment.event.author.fetchProfile().then((res) => setProfile(res)) }) const profileRoute = getProfilePageRoute( nip19.nprofileEncode({ - pubkey: props.pubkey + pubkey: comment.event.pubkey }) ) @@ -398,31 +363,33 @@ const Comment = (props: CommentEvent) => { {profile?.displayName || profile?.name || ''}{' '} - {hexToNpub(props.pubkey)} + {hexToNpub(comment.event.pubkey)} -
- - {formatDate(props.created_at * 1000, 'hh:mm aa')}{' '} - - - {formatDate(props.created_at * 1000, 'dd/MM/yyyy')} - -
+ {comment.event.created_at && ( +
+ + {formatDate(comment.event.created_at * 1000, 'hh:mm aa')}{' '} + + + {formatDate(comment.event.created_at * 1000, 'dd/MM/yyyy')} + +
+ )}
- {props.status && ( + {comment.status && (

Status: - {props.status} + {comment.status}

)} -

{props.content}

+

{comment.event.content}

- +
{
- -
- - - -

0

-

Replies

-
-
-

Reply

-
+ + {comment.event.kind === NDKKind.GenericReply && ( + <> +
+ + + +

0

+

Replies

+
+
+

Reply

+
+ + )}
) } -const Reactions = (props: Event) => { +const Reactions = (props: NostrEvent) => { const { isDataLoaded, likesCount, @@ -482,7 +453,7 @@ const Reactions = (props: Event) => { hasReactedNegatively } = useReactions({ pubkey: props.pubkey, - eTag: props.id + eTag: props.id! }) return ( @@ -537,7 +508,7 @@ const Reactions = (props: Event) => { ) } -const Zap = (props: Event) => { +const Zap = (props: NostrEvent) => { const [isOpen, setIsOpen] = useState(false) const [totalZappedAmount, setTotalZappedAmount] = useState(0) const [hasZapped, setHasZapped] = useState(false) @@ -550,7 +521,7 @@ const Zap = (props: Event) => { useDidMount(() => { getTotalZapAmount( props.pubkey, - props.id, + props.id!, undefined, userState.user?.pubkey as string ) diff --git a/src/hooks/useComments.ts b/src/hooks/useComments.ts index 4de0403..d741d26 100644 --- a/src/hooks/useComments.ts +++ b/src/hooks/useComments.ts @@ -48,7 +48,7 @@ export const useComments = ( }) const filter: NDKFilter = { - kinds: [NDKKind.Text], + kinds: [NDKKind.Text, NDKKind.GenericReply], '#a': [aTag] } @@ -73,21 +73,11 @@ export const useComments = ( subscription.on('event', (ndkEvent) => { setCommentEvents((prev) => { - if (prev.find((e) => e.id === ndkEvent.id)) { + if (prev.find((e) => e.event.id === ndkEvent.id)) { return [...prev] } - const commentEvent: CommentEvent = { - kind: NDKKind.Text, - tags: ndkEvent.tags, - content: ndkEvent.content, - created_at: ndkEvent.created_at!, - pubkey: ndkEvent.pubkey, - id: ndkEvent.id, - sig: ndkEvent.sig! - } - - return [commentEvent, ...prev] + return [{ event: ndkEvent }, ...prev] }) }) diff --git a/src/pages/blog/loader.ts b/src/pages/blog/loader.ts index bce006b..ab8439c 100644 --- a/src/pages/blog/loader.ts +++ b/src/pages/blog/loader.ts @@ -94,6 +94,7 @@ export const blogRouteLoader = ]) const result: BlogPageLoaderResult = { blog: undefined, + event: undefined, latest: [], isAddedToNSFW: false, isBlocked: false @@ -102,6 +103,9 @@ export const blogRouteLoader = // Check the blog event result const fetchEventResult = settled[0] if (fetchEventResult.status === 'fulfilled' && fetchEventResult.value) { + // Save original event + result.event = fetchEventResult.value + // Extract the blog details from the event result.blog = extractBlogDetails(fetchEventResult.value) } else if (fetchEventResult.status === 'rejected') { diff --git a/src/pages/mod/loader.ts b/src/pages/mod/loader.ts index a3c6fec..68c0594 100644 --- a/src/pages/mod/loader.ts +++ b/src/pages/mod/loader.ts @@ -103,6 +103,7 @@ export const modRouteLoader = const result: ModPageLoaderResult = { mod: undefined, + event: undefined, latest: [], isAddedToNSFW: false, isBlocked: false, @@ -112,6 +113,9 @@ export const modRouteLoader = // Check the mod event result const fetchEventResult = settled[0] if (fetchEventResult.status === 'fulfilled' && fetchEventResult.value) { + // Save original event + result.event = fetchEventResult.value + // Extract the mod data from the event result.mod = extractModData(fetchEventResult.value) } else if (fetchEventResult.status === 'rejected') { diff --git a/src/types/blog.ts b/src/types/blog.ts index 7e7fc18..44a83b0 100644 --- a/src/types/blog.ts +++ b/src/types/blog.ts @@ -1,3 +1,4 @@ +import { NDKEvent } from '@nostr-dev-kit/ndk' import { SortBy, NSFWFilter, ModeratedFilter } from './modsFilter' export interface BlogForm { @@ -36,6 +37,7 @@ export interface BlogCardDetails extends BlogDetails { export interface BlogPageLoaderResult { blog: Partial | undefined + event: NDKEvent | undefined latest: Partial[] isAddedToNSFW: boolean isBlocked: boolean diff --git a/src/types/mod.ts b/src/types/mod.ts index ae1da32..1055251 100644 --- a/src/types/mod.ts +++ b/src/types/mod.ts @@ -1,4 +1,4 @@ -import { Event } from 'nostr-tools' +import { NDKEvent } from '@nostr-dev-kit/ndk' import { BlogDetails } from 'types' export enum CommentEventStatus { @@ -7,7 +7,8 @@ export enum CommentEventStatus { Failed = 'Failed to publish comment.' } -export interface CommentEvent extends Event { +export interface CommentEvent { + event: NDKEvent status?: CommentEventStatus } @@ -85,6 +86,7 @@ export interface MuteLists { export interface ModPageLoaderResult { mod: ModDetails | undefined + event: NDKEvent | undefined latest: Partial[] isAddedToNSFW: boolean isBlocked: boolean