From 97b44a55f2ae065131b3f9324e7c7d8d4793ffe9 Mon Sep 17 00:00:00 2001 From: en Date: Tue, 28 Jan 2025 15:35:30 +0100 Subject: [PATCH 1/3] 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 From 11f42810673793ec5644190bee5c826b7a8e72a1 Mon Sep 17 00:00:00 2001 From: en Date: Tue, 28 Jan 2025 15:36:03 +0100 Subject: [PATCH 2/3] feat(ndk): use ndk nip07 signer --- src/layout/header.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/layout/header.tsx b/src/layout/header.tsx index 8b6b454..cfbb02a 100644 --- a/src/layout/header.tsx +++ b/src/layout/header.tsx @@ -22,10 +22,11 @@ import { npubToHex } from '../utils' import logo from '../assets/img/DEG Mods Logo With Text.svg' import placeholder from '../assets/img/DEG Mods Default PP.png' import { resetUserWot } from 'store/reducers/wot' +import { NDKNip07Signer } from '@nostr-dev-kit/ndk' export const Header = () => { const dispatch = useAppDispatch() - const { findMetadata } = useNDKContext() + const { findMetadata, ndk } = useNDKContext() const userState = useAppSelector((state) => state.user) const revalidator = useRevalidator() // Track nostr-login extension modal open state @@ -50,6 +51,7 @@ export const Header = () => { dispatch(setAuth(null)) dispatch(setUser(null)) dispatch(resetUserWot()) + ndk.signer = undefined } else { dispatch( setAuth({ @@ -63,6 +65,7 @@ export const Header = () => { pubkey: npubToHex(npub)! }) ) + ndk.signer = new NDKNip07Signer() findMetadata(npub).then((userProfile) => { if (userProfile) { dispatch( From 905b3ee5e4a2b587886482d3cf1d5cc903c9453b Mon Sep 17 00:00:00 2001 From: en Date: Wed, 29 Jan 2025 21:23:29 +0100 Subject: [PATCH 3/3] feat(comments): add popup, types, and utils, split components --- src/components/comment/Comment.tsx | 157 +++++++ src/components/comment/CommentContent.tsx | 18 + src/components/comment/CommentForm.tsx | 41 ++ src/components/comment/CommentsPopup.tsx | 279 +++++++++++++ src/components/comment/Filter.tsx | 79 ++++ src/components/comment/Reactions.tsx | 68 +++ src/components/comment/Zap.tsx | 76 ++++ src/components/comment/index.tsx | 478 +--------------------- src/constants.ts | 1 + src/hooks/useComments.ts | 20 +- src/hooks/useTextLimit.tsx | 19 + src/loaders/comment.ts | 53 +++ src/pages/blog/index.tsx | 6 +- src/pages/mod/index.tsx | 4 +- src/routes/index.tsx | 20 +- src/styles/comments.css | 2 + src/types/comments.ts | 16 + src/types/index.ts | 1 + src/utils/comments.ts | 91 ++++ src/utils/index.ts | 1 + src/utils/utils.ts | 5 + 21 files changed, 960 insertions(+), 475 deletions(-) create mode 100644 src/components/comment/Comment.tsx create mode 100644 src/components/comment/CommentContent.tsx create mode 100644 src/components/comment/CommentForm.tsx create mode 100644 src/components/comment/CommentsPopup.tsx create mode 100644 src/components/comment/Filter.tsx create mode 100644 src/components/comment/Reactions.tsx create mode 100644 src/components/comment/Zap.tsx create mode 100644 src/hooks/useTextLimit.tsx create mode 100644 src/loaders/comment.ts create mode 100644 src/types/comments.ts create mode 100644 src/utils/comments.ts diff --git a/src/components/comment/Comment.tsx b/src/components/comment/Comment.tsx new file mode 100644 index 0000000..46a2787 --- /dev/null +++ b/src/components/comment/Comment.tsx @@ -0,0 +1,157 @@ +import { NDKKind } from '@nostr-dev-kit/ndk' +import { formatDate } from 'date-fns' +import { useDidMount, useNDKContext } from 'hooks' +import { useState } from 'react' +import { useParams, useLocation, Link } from 'react-router-dom' +import { getModPageRoute, getBlogPageRoute, getProfilePageRoute } from 'routes' +import { CommentEvent, UserProfile } from 'types' +import { hexToNpub } from 'utils' +import { Reactions } from './Reactions' +import { Zap } from './Zap' +import { nip19 } from 'nostr-tools' +import { CommentContent } from './CommentContent' + +interface CommentProps { + comment: CommentEvent +} +export const Comment = ({ comment }: CommentProps) => { + const { naddr } = useParams() + const location = useLocation() + const { ndk } = useNDKContext() + const isMod = location.pathname.includes('/mod/') + const isBlog = location.pathname.includes('/blog/') + const baseUrl = naddr + ? isMod + ? getModPageRoute(naddr) + : isBlog + ? getBlogPageRoute(naddr) + : undefined + : undefined + const [commentEvents, setCommentEvents] = useState([]) + const [profile, setProfile] = useState() + + useDidMount(() => { + comment.event.author.fetchProfile().then((res) => setProfile(res)) + ndk + .fetchEvents({ + kinds: [NDKKind.Text, NDKKind.GenericReply], + '#e': [comment.event.id] + }) + .then((ndkEventsSet) => { + setCommentEvents( + Array.from(ndkEventsSet).map((ndkEvent) => ({ + event: ndkEvent + })) + ) + }) + }) + + const profileRoute = getProfilePageRoute( + nip19.nprofileEncode({ + pubkey: comment.event.pubkey + }) + ) + + return ( +
+
+
+ +
+
+
+ + {profile?.displayName || profile?.name || ''}{' '} + + + {hexToNpub(comment.event.pubkey)} + +
+ {comment.event.created_at && ( + + )} +
+
+
+ {comment.status && ( +

+ Status: + {comment.status} +

+ )} + +
+
+
+ +
+ + + +

0

+
+
+
+
+ {typeof profile?.lud16 !== 'undefined' && profile.lud16 !== '' && ( + + )} + {comment.event.kind === NDKKind.GenericReply && ( + <> + + + + +

+ {commentEvents.length} +

+

Replies

+ + +

Reply

+ + + )} +
+
+
+ ) +} diff --git a/src/components/comment/CommentContent.tsx b/src/components/comment/CommentContent.tsx new file mode 100644 index 0000000..292a93f --- /dev/null +++ b/src/components/comment/CommentContent.tsx @@ -0,0 +1,18 @@ +import { useTextLimit } from 'hooks/useTextLimit' +interface CommentContentProps { + content: string +} +export const CommentContent = ({ content }: CommentContentProps) => { + const { text, isTextOverflowing, isExpanded, toggle } = useTextLimit(content) + + return ( + <> +

{text}

+ {isTextOverflowing && ( +
+

{isExpanded ? 'Hide' : 'View'} full post

+
+ )} + + ) +} diff --git a/src/components/comment/CommentForm.tsx b/src/components/comment/CommentForm.tsx new file mode 100644 index 0000000..3a36e10 --- /dev/null +++ b/src/components/comment/CommentForm.tsx @@ -0,0 +1,41 @@ +import { useState } from 'react' + +type CommentFormProps = { + handleSubmit: (content: string) => Promise +} + +export const CommentForm = ({ handleSubmit }: CommentFormProps) => { + const [isSubmitting, setIsSubmitting] = useState(false) + const [commentText, setCommentText] = useState('') + + const handleComment = async () => { + setIsSubmitting(true) + const submitted = await handleSubmit(commentText) + if (submitted) setCommentText('') + setIsSubmitting(false) + } + + return ( +
+
+ +
+
+ {/* Quote-Repost */} + +
+
+ {commentEvents.length > 0 && ( + <> +

+ Replies +

+
+ {commentEvents.map((reply) => ( + + ))} +
+ + )} + + + + + + ) +} diff --git a/src/components/comment/Filter.tsx b/src/components/comment/Filter.tsx new file mode 100644 index 0000000..b0774e9 --- /dev/null +++ b/src/components/comment/Filter.tsx @@ -0,0 +1,79 @@ +import React, { Dispatch, SetStateAction } from 'react' +import { AuthorFilterEnum, SortByEnum } from 'types' + +export type FilterOptions = { + sort: SortByEnum + author: AuthorFilterEnum +} + +type FilterProps = { + filterOptions: FilterOptions + setFilterOptions: Dispatch> +} + +export const Filter = React.memo( + ({ filterOptions, setFilterOptions }: FilterProps) => { + return ( +
+
+
+ + +
+ {Object.values(SortByEnum).map((item) => ( +
+ setFilterOptions((prev) => ({ + ...prev, + sort: item + })) + } + > + {item} +
+ ))} +
+
+
+
+
+ + +
+ {Object.values(AuthorFilterEnum).map((item) => ( +
+ setFilterOptions((prev) => ({ + ...prev, + author: item + })) + } + > + {item} +
+ ))} +
+
+
+
+ ) + } +) diff --git a/src/components/comment/Reactions.tsx b/src/components/comment/Reactions.tsx new file mode 100644 index 0000000..dec7563 --- /dev/null +++ b/src/components/comment/Reactions.tsx @@ -0,0 +1,68 @@ +import { NostrEvent } from '@nostr-dev-kit/ndk' +import { Dots } from 'components/Spinner' +import { useReactions } from 'hooks' + +export const Reactions = (props: NostrEvent) => { + const { + isDataLoaded, + likesCount, + disLikesCount, + handleReaction, + hasReactedPositively, + hasReactedNegatively + } = useReactions({ + pubkey: props.pubkey, + eTag: props.id! + }) + + return ( + <> +
handleReaction(true) : undefined} + > + + + +

+ {isDataLoaded ? likesCount : } +

+
+
+
+
+
handleReaction() : undefined} + > + + + +

+ {isDataLoaded ? disLikesCount : } +

+
+
+
+
+ + ) +} diff --git a/src/components/comment/Zap.tsx b/src/components/comment/Zap.tsx new file mode 100644 index 0000000..0698035 --- /dev/null +++ b/src/components/comment/Zap.tsx @@ -0,0 +1,76 @@ +import { NostrEvent } from '@nostr-dev-kit/ndk' +import { ZapPopUp } from 'components/Zap' +import { + useAppSelector, + useNDKContext, + useBodyScrollDisable, + useDidMount +} from 'hooks' +import { useState } from 'react' +import { toast } from 'react-toastify' +import { abbreviateNumber } from 'utils' + +export const Zap = (props: NostrEvent) => { + const [isOpen, setIsOpen] = useState(false) + const [totalZappedAmount, setTotalZappedAmount] = useState(0) + const [hasZapped, setHasZapped] = useState(false) + + const userState = useAppSelector((state) => state.user) + const { getTotalZapAmount } = useNDKContext() + + useBodyScrollDisable(isOpen) + + useDidMount(() => { + getTotalZapAmount( + props.pubkey, + props.id!, + undefined, + userState.user?.pubkey as string + ) + .then((res) => { + setTotalZappedAmount(res.accumulatedZapAmount) + setHasZapped(res.hasZapped) + }) + .catch((err) => { + toast.error(err.message || err) + }) + }) + + return ( + <> +
setIsOpen(true)} + > + + + +

+ {abbreviateNumber(totalZappedAmount)} +

+
+
+
+
+ {isOpen && ( + setIsOpen(false)} + setTotalZapAmount={setTotalZappedAmount} + setHasZapped={setHasZapped} + /> + )} + + ) +} diff --git a/src/components/comment/index.tsx b/src/components/comment/index.tsx index 7190fb1..6edcdaf 100644 --- a/src/components/comment/index.tsx +++ b/src/components/comment/index.tsx @@ -1,50 +1,20 @@ -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' -import { - useAppSelector, - useBodyScrollDisable, - useDidMount, - useNDKContext, - useReactions -} from 'hooks' +import { Spinner } from 'components/Spinner' +import { useNDKContext } from 'hooks' import { useComments } from 'hooks/useComments' -import { nip19 } from 'nostr-tools' -import React, { - Dispatch, - SetStateAction, - useEffect, - useMemo, - useState -} from 'react' -import { Link, useLoaderData } from 'react-router-dom' -import { toast } from 'react-toastify' -import { getProfilePageRoute } from 'routes' +import { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react' +import { useLoaderData } from 'react-router-dom' import { Addressable, + AuthorFilterEnum, BlogPageLoaderResult, CommentEvent, - CommentEventStatus, ModPageLoaderResult, - UserProfile -} from 'types/index.ts' -import { abbreviateNumber, hexToNpub, log, LogType } from 'utils' - -enum SortByEnum { - Latest = 'Latest', - Oldest = 'Oldest' -} - -enum AuthorFilterEnum { - All_Comments = 'All Comments', - Creator_Comments = 'Creator Comments' -} - -type FilterOptions = { - sort: SortByEnum - author: AuthorFilterEnum -} + SortByEnum +} from 'types' +import { handleCommentSubmit } from 'utils' +import { Filter, FilterOptions } from './Filter' +import { CommentForm } from './CommentForm' +import { Comment } from './Comment' type Props = { addressable: Addressable @@ -78,74 +48,7 @@ export const Comments = ({ addressable, setCommentCount }: Props) => { setCommentCount(commentEvents.length) }, [commentEvents, setCommentCount]) - const handleSubmit = async (content: string): Promise => { - if (content === '') return false - - // NDKEvent required - if (!event) return false - - try { - const reply = event.reply() - reply.content = content - - 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 ce - }) - ) - // when an event is successfully published remove the status from it after 15 seconds - setTimeout(() => { - setCommentEvents((prev) => - prev.map((ce) => { - if (ce.event.id === reply.id) { - delete ce.status - } - - return ce - }) - ) - }, 15000) - } else { - log(true, LogType.Error, 'Publishing comment failed.') - setCommentEvents((prev) => - prev.map((ce) => { - if (ce.event.id === reply.id) { - return { - event: ce.event, - status: CommentEventStatus.Failed - } - } - return ce - }) - ) - } - 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 handleSubmit = handleCommentSubmit(event, setCommentEvents, ndk) const handleDiscoveredClick = () => { setVisible(commentEvents) @@ -215,360 +118,3 @@ export const Comments = ({ addressable, setCommentCount }: Props) => { ) } - -type CommentFormProps = { - handleSubmit: (content: string) => Promise -} - -const CommentForm = ({ handleSubmit }: CommentFormProps) => { - const [isSubmitting, setIsSubmitting] = useState(false) - const [commentText, setCommentText] = useState('') - - const handleComment = async () => { - setIsSubmitting(true) - const submitted = await handleSubmit(commentText) - if (submitted) setCommentText('') - setIsSubmitting(false) - } - - return ( -
-
-