feat(reply): publish new reply with ndkevent, fetch kind 1 and 1111

This commit is contained in:
en 2025-01-28 15:35:30 +01:00
parent 612524741b
commit 97b44a55f2
6 changed files with 139 additions and 166 deletions

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 { 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<FilterOptions>({
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<boolean> => {
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}
/>
<div className='IBMSMSMBSSCommentsList'>
{comments.map((event) => (
<Comment key={event.id} {...event} />
{comments.map((comment) => (
<Comment key={comment.event.id} comment={comment} />
))}
</div>
</div>
@ -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<UserProfile>()
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 || ''}{' '}
</Link>
<Link className='IBMSMSMBSSCL_CTD_Address' to={profileRoute}>
{hexToNpub(props.pubkey)}
{hexToNpub(comment.event.pubkey)}
</Link>
</div>
<div className='IBMSMSMBSSCL_CommentActionsDetails'>
<a className='IBMSMSMBSSCL_CADTime'>
{formatDate(props.created_at * 1000, 'hh:mm aa')}{' '}
</a>
<a className='IBMSMSMBSSCL_CADDate'>
{formatDate(props.created_at * 1000, 'dd/MM/yyyy')}
</a>
</div>
{comment.event.created_at && (
<div className='IBMSMSMBSSCL_CommentActionsDetails'>
<a className='IBMSMSMBSSCL_CADTime'>
{formatDate(comment.event.created_at * 1000, 'hh:mm aa')}{' '}
</a>
<a className='IBMSMSMBSSCL_CADDate'>
{formatDate(comment.event.created_at * 1000, 'dd/MM/yyyy')}
</a>
</div>
)}
</div>
</div>
<div className='IBMSMSMBSSCL_CommentBottom'>
{props.status && (
{comment.status && (
<p className='IBMSMSMBSSCL_CBTextStatus'>
<span className='IBMSMSMBSSCL_CBTextStatusSpan'>Status:</span>
{props.status}
{comment.status}
</p>
)}
<p className='IBMSMSMBSSCL_CBText'>{props.content}</p>
<p className='IBMSMSMBSSCL_CBText'>{comment.event.content}</p>
</div>
<div className='IBMSMSMBSSCL_CommentActions'>
<div className='IBMSMSMBSSCL_CommentActionsInside'>
<Reactions {...props} />
<Reactions {...comment.event.rawEvent()} />
<div
className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAERepost'
style={{ cursor: 'not-allowed' }}
@ -442,37 +409,41 @@ const Comment = (props: CommentEvent) => {
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
</div>
</div>
<Zap {...props} />
<div
className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEReplies'
style={{ cursor: 'not-allowed' }}
>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
width='1em'
height='1em'
fill='currentColor'
className='IBMSMSMBSSCL_CAElementIcon'
>
<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>
</svg>
<p className='IBMSMSMBSSCL_CAElementText'>0</p>
<p className='IBMSMSMBSSCL_CAElementText'>Replies</p>
</div>
<div
className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEReply'
style={{ cursor: 'not-allowed' }}
>
<p className='IBMSMSMBSSCL_CAElementText'>Reply</p>
</div>
<Zap {...comment.event.rawEvent()} />
{comment.event.kind === NDKKind.GenericReply && (
<>
<div
className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEReplies'
style={{ cursor: 'not-allowed' }}
>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
width='1em'
height='1em'
fill='currentColor'
className='IBMSMSMBSSCL_CAElementIcon'
>
<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>
</svg>
<p className='IBMSMSMBSSCL_CAElementText'>0</p>
<p className='IBMSMSMBSSCL_CAElementText'>Replies</p>
</div>
<div
className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEReply'
style={{ cursor: 'not-allowed' }}
>
<p className='IBMSMSMBSSCL_CAElementText'>Reply</p>
</div>
</>
)}
</div>
</div>
</div>
)
}
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
)

View File

@ -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]
})
})

View File

@ -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') {

View File

@ -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') {

View File

@ -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<BlogDetails> | undefined
event: NDKEvent | undefined
latest: Partial<BlogDetails>[]
isAddedToNSFW: 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'
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<BlogDetails>[]
isAddedToNSFW: boolean
isBlocked: boolean