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 acb7c0f..6edcdaf 100644 --- a/src/components/comment/index.tsx +++ b/src/components/comment/index.tsx @@ -1,48 +1,20 @@ -import { NDKEvent } 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 { Event, kinds, nip19, UnsignedEvent } from 'nostr-tools' -import React, { - Dispatch, - SetStateAction, - useEffect, - useMemo, - useState -} from 'react' -import { Link } 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, - UserProfile -} from 'types/index.ts' -import { abbreviateNumber, hexToNpub, log, LogType, now } from 'utils' - -enum SortByEnum { - Latest = 'Latest', - Oldest = 'Oldest' -} - -enum AuthorFilterEnum { - All_Comments = 'All Comments', - Creator_Comments = 'Creator Comments' -} - -type FilterOptions = { - sort: SortByEnum - author: AuthorFilterEnum -} + ModPageLoaderResult, + SortByEnum +} from 'types' +import { handleCommentSubmit } from 'utils' +import { Filter, FilterOptions } from './Filter' +import { CommentForm } from './CommentForm' +import { Comment } from './Comment' type Props = { addressable: Addressable @@ -50,11 +22,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,121 +48,7 @@ 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 - - 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) - } - } - - 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 - } - } - - return event - }) - ) - } else { - setCommentEvents((prev) => - prev.map((event) => { - if (event.id === signedEvent.id) { - return { - ...event, - status: CommentEventStatus.Published - } - } - - return event - }) - ) - } - - // 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 - } - - return event - }) - ) - }, 15000) - }) - .catch((err) => { - console.error('An error occurred in publishing comment', err) - setCommentEvents((prev) => - prev.map((event) => { - if (event.id === signedEvent.id) { - return { - ...event, - status: CommentEventStatus.Failed - } - } - - return event - }) - ) - }) - - return true - } + const handleSubmit = handleCommentSubmit(event, setCommentEvents, ndk) const handleDiscoveredClick = () => { setVisible(commentEvents) @@ -203,14 +64,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,363 +110,11 @@ export const Comments = ({ addressable, setCommentCount }: Props) => { setFilterOptions={setFilterOptions} />
- {comments.map((event) => ( - + {comments.map((comment) => ( + ))}
) } - -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 ( -
-
-