From 905b3ee5e4a2b587886482d3cf1d5cc903c9453b Mon Sep 17 00:00:00 2001 From: en Date: Wed, 29 Jan 2025 21:23:29 +0100 Subject: [PATCH] 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 ( -
-
-