Adv. comments #207

Merged
enes merged 3 commits from feat/130-adv-comments into staging 2025-01-29 20:53:33 +00:00
6 changed files with 139 additions and 166 deletions
Showing only changes of commit 97b44a55f2 - Show all commits

View File

@ -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 { Dots, Spinner } from 'components/Spinner'
import { ZapPopUp } from 'components/Zap' import { ZapPopUp } from 'components/Zap'
import { formatDate } from 'date-fns' import { formatDate } from 'date-fns'
@ -10,7 +10,7 @@ import {
useReactions useReactions
} from 'hooks' } from 'hooks'
import { useComments } from 'hooks/useComments' import { useComments } from 'hooks/useComments'
import { Event, kinds, nip19, UnsignedEvent } from 'nostr-tools' import { nip19 } from 'nostr-tools'
import React, { import React, {
Dispatch, Dispatch,
SetStateAction, SetStateAction,
@ -18,16 +18,18 @@ import React, {
useMemo, useMemo,
useState useState
} from 'react' } from 'react'
import { Link } from 'react-router-dom' import { Link, useLoaderData } from 'react-router-dom'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { getProfilePageRoute } from 'routes' import { getProfilePageRoute } from 'routes'
import { import {
Addressable, Addressable,
BlogPageLoaderResult,
CommentEvent, CommentEvent,
CommentEventStatus, CommentEventStatus,
ModPageLoaderResult,
UserProfile UserProfile
} from 'types/index.ts' } from 'types/index.ts'
import { abbreviateNumber, hexToNpub, log, LogType, now } from 'utils' import { abbreviateNumber, hexToNpub, log, LogType } from 'utils'
enum SortByEnum { enum SortByEnum {
Latest = 'Latest', Latest = 'Latest',
@ -50,11 +52,14 @@ type Props = {
} }
export const Comments = ({ addressable, setCommentCount }: Props) => { export const Comments = ({ addressable, setCommentCount }: Props) => {
const { ndk, publish } = useNDKContext() const { ndk } = useNDKContext()
const { commentEvents, setCommentEvents } = useComments( const { commentEvents, setCommentEvents } = useComments(
addressable.author, addressable.author,
addressable.aTag addressable.aTag
) )
const { event } = useLoaderData() as
| ModPageLoaderResult
| BlogPageLoaderResult
const [filterOptions, setFilterOptions] = useState<FilterOptions>({ const [filterOptions, setFilterOptions] = useState<FilterOptions>({
sort: SortByEnum.Latest, sort: SortByEnum.Latest,
author: AuthorFilterEnum.All_Comments author: AuthorFilterEnum.All_Comments
@ -73,120 +78,73 @@ export const Comments = ({ addressable, setCommentCount }: Props) => {
setCommentCount(commentEvents.length) setCommentCount(commentEvents.length)
}, [commentEvents, setCommentCount]) }, [commentEvents, setCommentCount])
const userState = useAppSelector((state) => state.user)
const handleSubmit = async (content: string): Promise<boolean> => { const handleSubmit = async (content: string): Promise<boolean> => {
if (content === '') return false if (content === '') return false
let pubkey: string | undefined // NDKEvent required
if (!event) return false
if (userState.auth && userState.user?.pubkey) { try {
pubkey = userState.user.pubkey as string const reply = event.reply()
} else { reply.content = content
try {
pubkey = (await window.nostr?.getPublicKey()) as string
} catch (error) {
log(true, LogType.Error, `Could not get pubkey`, error)
}
}
if (!pubkey) { setCommentEvents((prev) => [
toast.error('Could not get user pubkey') {
return false event: new NDKEvent(ndk, reply),
} status: CommentEventStatus.Publishing
},
const unsignedEvent: UnsignedEvent = { ...prev
content: content, ])
pubkey: pubkey, const relaySet = await reply.publish()
kind: kinds.ShortTextNote, if (relaySet.size) {
created_at: now(), setCommentEvents((prev) =>
tags: [ prev.map((ce) => {
['e', addressable.id], if (ce.event.id === reply.id) {
['a', addressable.aTag], return {
['p', addressable.author] event: ce.event,
] status: CommentEventStatus.Published
}
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 return ce
}) })
) )
} 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 // when an event is successfully published remove the status from it after 15 seconds
setTimeout(() => { setTimeout(() => {
setCommentEvents((prev) => setCommentEvents((prev) =>
prev.map((event) => { prev.map((ce) => {
if (event.id === signedEvent.id) { if (ce.event.id === reply.id) {
delete event.status delete ce.status
} }
return event return ce
}) })
) )
}, 15000) }, 15000)
}) } else {
.catch((err) => { log(true, LogType.Error, 'Publishing comment failed.')
console.error('An error occurred in publishing comment', err)
setCommentEvents((prev) => setCommentEvents((prev) =>
prev.map((event) => { prev.map((ce) => {
if (event.id === signedEvent.id) { if (ce.event.id === reply.id) {
return { return {
...event, event: ce.event,
status: CommentEventStatus.Failed status: CommentEventStatus.Failed
} }
} }
return ce
return event
}) })
) )
}) }
return false
return true } 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 = () => { const handleDiscoveredClick = () => {
@ -203,14 +161,22 @@ export const Comments = ({ addressable, setCommentCount }: Props) => {
let filteredComments = visible let filteredComments = visible
if (filterOptions.author === AuthorFilterEnum.Creator_Comments) { if (filterOptions.author === AuthorFilterEnum.Creator_Comments) {
filteredComments = filteredComments.filter( filteredComments = filteredComments.filter(
(comment) => comment.pubkey === addressable.author (comment) => comment.event.pubkey === addressable.author
) )
} }
if (filterOptions.sort === SortByEnum.Latest) { 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) { } 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 return filteredComments
@ -241,8 +207,8 @@ export const Comments = ({ addressable, setCommentCount }: Props) => {
setFilterOptions={setFilterOptions} setFilterOptions={setFilterOptions}
/> />
<div className='IBMSMSMBSSCommentsList'> <div className='IBMSMSMBSSCommentsList'>
{comments.map((event) => ( {comments.map((comment) => (
<Comment key={event.id} {...event} /> <Comment key={comment.event.id} comment={comment} />
))} ))}
</div> </div>
</div> </div>
@ -361,20 +327,19 @@ const Filter = React.memo(
) )
} }
) )
interface CommentProps {
const Comment = (props: CommentEvent) => { comment: CommentEvent
const { findMetadata } = useNDKContext() }
const Comment = ({ comment }: CommentProps) => {
const [profile, setProfile] = useState<UserProfile>() const [profile, setProfile] = useState<UserProfile>()
useDidMount(() => { useDidMount(() => {
findMetadata(props.pubkey).then((res) => { comment.event.author.fetchProfile().then((res) => setProfile(res))
setProfile(res)
})
}) })
const profileRoute = getProfilePageRoute( const profileRoute = getProfilePageRoute(
nip19.nprofileEncode({ nip19.nprofileEncode({
pubkey: props.pubkey pubkey: comment.event.pubkey
}) })
) )
@ -398,31 +363,33 @@ const Comment = (props: CommentEvent) => {
{profile?.displayName || profile?.name || ''}{' '} {profile?.displayName || profile?.name || ''}{' '}
</Link> </Link>
<Link className='IBMSMSMBSSCL_CTD_Address' to={profileRoute}> <Link className='IBMSMSMBSSCL_CTD_Address' to={profileRoute}>
{hexToNpub(props.pubkey)} {hexToNpub(comment.event.pubkey)}
</Link> </Link>
</div> </div>
<div className='IBMSMSMBSSCL_CommentActionsDetails'> {comment.event.created_at && (
<a className='IBMSMSMBSSCL_CADTime'> <div className='IBMSMSMBSSCL_CommentActionsDetails'>
{formatDate(props.created_at * 1000, 'hh:mm aa')}{' '} <a className='IBMSMSMBSSCL_CADTime'>
</a> {formatDate(comment.event.created_at * 1000, 'hh:mm aa')}{' '}
<a className='IBMSMSMBSSCL_CADDate'> </a>
{formatDate(props.created_at * 1000, 'dd/MM/yyyy')} <a className='IBMSMSMBSSCL_CADDate'>
</a> {formatDate(comment.event.created_at * 1000, 'dd/MM/yyyy')}
</div> </a>
</div>
)}
</div> </div>
</div> </div>
<div className='IBMSMSMBSSCL_CommentBottom'> <div className='IBMSMSMBSSCL_CommentBottom'>
{props.status && ( {comment.status && (
<p className='IBMSMSMBSSCL_CBTextStatus'> <p className='IBMSMSMBSSCL_CBTextStatus'>
<span className='IBMSMSMBSSCL_CBTextStatusSpan'>Status:</span> <span className='IBMSMSMBSSCL_CBTextStatusSpan'>Status:</span>
{props.status} {comment.status}
</p> </p>
)} )}
<p className='IBMSMSMBSSCL_CBText'>{props.content}</p> <p className='IBMSMSMBSSCL_CBText'>{comment.event.content}</p>
</div> </div>
<div className='IBMSMSMBSSCL_CommentActions'> <div className='IBMSMSMBSSCL_CommentActions'>
<div className='IBMSMSMBSSCL_CommentActionsInside'> <div className='IBMSMSMBSSCL_CommentActionsInside'>
<Reactions {...props} /> <Reactions {...comment.event.rawEvent()} />
<div <div
className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAERepost' className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAERepost'
style={{ cursor: 'not-allowed' }} style={{ cursor: 'not-allowed' }}
@ -442,37 +409,41 @@ const Comment = (props: CommentEvent) => {
<div className='IBMSMSMBSSCL_CAElementLoad'></div> <div className='IBMSMSMBSSCL_CAElementLoad'></div>
</div> </div>
</div> </div>
<Zap {...props} /> <Zap {...comment.event.rawEvent()} />
<div {comment.event.kind === NDKKind.GenericReply && (
className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEReplies' <>
style={{ cursor: 'not-allowed' }} <div
> className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEReplies'
<svg style={{ cursor: 'not-allowed' }}
xmlns='http://www.w3.org/2000/svg' >
viewBox='0 0 512 512' <svg
width='1em' xmlns='http://www.w3.org/2000/svg'
height='1em' viewBox='0 0 512 512'
fill='currentColor' width='1em'
className='IBMSMSMBSSCL_CAElementIcon' height='1em'
> fill='currentColor'
<path d='M256 32C114.6 32 .0272 125.1 .0272 240c0 49.63 21.35 94.98 56.97 130.7c-12.5 50.37-54.27 95.27-54.77 95.77c-2.25 2.25-2.875 5.734-1.5 8.734C1.979 478.2 4.75 480 8 480c66.25 0 115.1-31.76 140.6-51.39C181.2 440.9 217.6 448 256 448c141.4 0 255.1-93.13 255.1-208S397.4 32 256 32z'></path> className='IBMSMSMBSSCL_CAElementIcon'
</svg> >
<p className='IBMSMSMBSSCL_CAElementText'>0</p> <path d='M256 32C114.6 32 .0272 125.1 .0272 240c0 49.63 21.35 94.98 56.97 130.7c-12.5 50.37-54.27 95.27-54.77 95.77c-2.25 2.25-2.875 5.734-1.5 8.734C1.979 478.2 4.75 480 8 480c66.25 0 115.1-31.76 140.6-51.39C181.2 440.9 217.6 448 256 448c141.4 0 255.1-93.13 255.1-208S397.4 32 256 32z'></path>
<p className='IBMSMSMBSSCL_CAElementText'>Replies</p> </svg>
</div> <p className='IBMSMSMBSSCL_CAElementText'>0</p>
<div <p className='IBMSMSMBSSCL_CAElementText'>Replies</p>
className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEReply' </div>
style={{ cursor: 'not-allowed' }} <div
> className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEReply'
<p className='IBMSMSMBSSCL_CAElementText'>Reply</p> style={{ cursor: 'not-allowed' }}
</div> >
<p className='IBMSMSMBSSCL_CAElementText'>Reply</p>
</div>
</>
)}
</div> </div>
</div> </div>
</div> </div>
) )
} }
const Reactions = (props: Event) => { const Reactions = (props: NostrEvent) => {
const { const {
isDataLoaded, isDataLoaded,
likesCount, likesCount,
@ -482,7 +453,7 @@ const Reactions = (props: Event) => {
hasReactedNegatively hasReactedNegatively
} = useReactions({ } = useReactions({
pubkey: props.pubkey, pubkey: props.pubkey,
eTag: props.id eTag: props.id!
}) })
return ( return (
@ -537,7 +508,7 @@ const Reactions = (props: Event) => {
) )
} }
const Zap = (props: Event) => { const Zap = (props: NostrEvent) => {
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const [totalZappedAmount, setTotalZappedAmount] = useState(0) const [totalZappedAmount, setTotalZappedAmount] = useState(0)
const [hasZapped, setHasZapped] = useState(false) const [hasZapped, setHasZapped] = useState(false)
@ -550,7 +521,7 @@ const Zap = (props: Event) => {
useDidMount(() => { useDidMount(() => {
getTotalZapAmount( getTotalZapAmount(
props.pubkey, props.pubkey,
props.id, props.id!,
undefined, undefined,
userState.user?.pubkey as string userState.user?.pubkey as string
) )

View File

@ -48,7 +48,7 @@ export const useComments = (
}) })
const filter: NDKFilter = { const filter: NDKFilter = {
kinds: [NDKKind.Text], kinds: [NDKKind.Text, NDKKind.GenericReply],
'#a': [aTag] '#a': [aTag]
} }
@ -73,21 +73,11 @@ export const useComments = (
subscription.on('event', (ndkEvent) => { subscription.on('event', (ndkEvent) => {
setCommentEvents((prev) => { setCommentEvents((prev) => {
if (prev.find((e) => e.id === ndkEvent.id)) { if (prev.find((e) => e.event.id === ndkEvent.id)) {
return [...prev] return [...prev]
} }
const commentEvent: CommentEvent = { return [{ event: ndkEvent }, ...prev]
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]
}) })
}) })

View File

@ -94,6 +94,7 @@ export const blogRouteLoader =
]) ])
const result: BlogPageLoaderResult = { const result: BlogPageLoaderResult = {
blog: undefined, blog: undefined,
event: undefined,
latest: [], latest: [],
isAddedToNSFW: false, isAddedToNSFW: false,
isBlocked: false isBlocked: false
@ -102,6 +103,9 @@ export const blogRouteLoader =
// Check the blog event result // Check the blog event result
const fetchEventResult = settled[0] const fetchEventResult = settled[0]
if (fetchEventResult.status === 'fulfilled' && fetchEventResult.value) { if (fetchEventResult.status === 'fulfilled' && fetchEventResult.value) {
// Save original event
result.event = fetchEventResult.value
// Extract the blog details from the event // Extract the blog details from the event
result.blog = extractBlogDetails(fetchEventResult.value) result.blog = extractBlogDetails(fetchEventResult.value)
} else if (fetchEventResult.status === 'rejected') { } else if (fetchEventResult.status === 'rejected') {

View File

@ -103,6 +103,7 @@ export const modRouteLoader =
const result: ModPageLoaderResult = { const result: ModPageLoaderResult = {
mod: undefined, mod: undefined,
event: undefined,
latest: [], latest: [],
isAddedToNSFW: false, isAddedToNSFW: false,
isBlocked: false, isBlocked: false,
@ -112,6 +113,9 @@ export const modRouteLoader =
// Check the mod event result // Check the mod event result
const fetchEventResult = settled[0] const fetchEventResult = settled[0]
if (fetchEventResult.status === 'fulfilled' && fetchEventResult.value) { if (fetchEventResult.status === 'fulfilled' && fetchEventResult.value) {
// Save original event
result.event = fetchEventResult.value
// Extract the mod data from the event // Extract the mod data from the event
result.mod = extractModData(fetchEventResult.value) result.mod = extractModData(fetchEventResult.value)
} else if (fetchEventResult.status === 'rejected') { } else if (fetchEventResult.status === 'rejected') {

View File

@ -1,3 +1,4 @@
import { NDKEvent } from '@nostr-dev-kit/ndk'
import { SortBy, NSFWFilter, ModeratedFilter } from './modsFilter' import { SortBy, NSFWFilter, ModeratedFilter } from './modsFilter'
export interface BlogForm { export interface BlogForm {
@ -36,6 +37,7 @@ export interface BlogCardDetails extends BlogDetails {
export interface BlogPageLoaderResult { export interface BlogPageLoaderResult {
blog: Partial<BlogDetails> | undefined blog: Partial<BlogDetails> | undefined
event: NDKEvent | undefined
latest: Partial<BlogDetails>[] latest: Partial<BlogDetails>[]
isAddedToNSFW: boolean isAddedToNSFW: boolean
isBlocked: boolean isBlocked: boolean

View File

@ -1,4 +1,4 @@
import { Event } from 'nostr-tools' import { NDKEvent } from '@nostr-dev-kit/ndk'
import { BlogDetails } from 'types' import { BlogDetails } from 'types'
export enum CommentEventStatus { export enum CommentEventStatus {
@ -7,7 +7,8 @@ export enum CommentEventStatus {
Failed = 'Failed to publish comment.' Failed = 'Failed to publish comment.'
} }
export interface CommentEvent extends Event { export interface CommentEvent {
event: NDKEvent
status?: CommentEventStatus status?: CommentEventStatus
} }
@ -85,6 +86,7 @@ export interface MuteLists {
export interface ModPageLoaderResult { export interface ModPageLoaderResult {
mod: ModDetails | undefined mod: ModDetails | undefined
event: NDKEvent | undefined
latest: Partial<BlogDetails>[] latest: Partial<BlogDetails>[]
isAddedToNSFW: boolean isAddedToNSFW: boolean
isBlocked: boolean isBlocked: boolean