fix: partial replies not being displayed
This commit is contained in:
@@ -38,7 +38,12 @@ export default function ReplyNote({
|
||||
<Content event={event} size="small" />
|
||||
<div className="flex gap-2 text-xs">
|
||||
<div className="text-muted-foreground/60">{formatTimestamp(event.created_at)}</div>
|
||||
<div className="text-muted-foreground hover:text-primary cursor-pointer">reply</div>
|
||||
<div
|
||||
className="text-muted-foreground hover:text-primary cursor-pointer"
|
||||
onClick={() => setIsPostDialogOpen(true)}
|
||||
>
|
||||
reply
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<LikeButton event={event} variant="reply" />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Separator } from '@renderer/components/ui/separator'
|
||||
import { getParentEventId, isReplyNoteEvent } from '@renderer/lib/event'
|
||||
import { isReplyNoteEvent } from '@renderer/lib/event'
|
||||
import { isReplyETag, isRootETag } from '@renderer/lib/tag'
|
||||
import { cn } from '@renderer/lib/utils'
|
||||
import { useNoteStats } from '@renderer/providers/NoteStatsProvider'
|
||||
import client from '@renderer/services/client.service'
|
||||
@@ -9,8 +10,10 @@ import { useEffect, useRef, useState } from 'react'
|
||||
import ReplyNote from '../ReplyNote'
|
||||
|
||||
export default function ReplyNoteList({ event, className }: { event: Event; className?: string }) {
|
||||
const [eventsWithParentIds, setEventsWithParentId] = useState<[Event, string | undefined][]>([])
|
||||
const [eventMap, setEventMap] = useState<Record<string, Event>>({})
|
||||
const [replies, setReplies] = useState<Event[]>([])
|
||||
const [replyMap, setReplyMap] = useState<
|
||||
Record<string, { event: Event; level: number; parent?: Event } | undefined>
|
||||
>({})
|
||||
const [until, setUntil] = useState<number>(() => dayjs().unix())
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [hasMore, setHasMore] = useState<boolean>(false)
|
||||
@@ -30,13 +33,7 @@ export default function ReplyNoteList({ event, className }: { event: Event; clas
|
||||
const sortedEvents = events.sort((a, b) => a.created_at - b.created_at)
|
||||
const processedEvents = events.filter((e) => isReplyNoteEvent(e))
|
||||
if (processedEvents.length > 0) {
|
||||
const eventMap: Record<string, Event> = {}
|
||||
const eventsWithParentIds = processedEvents.map((event) => {
|
||||
eventMap[event.id] = event
|
||||
return [event, getParentEventId(event)] as [Event, string | undefined]
|
||||
})
|
||||
setEventsWithParentId((pre) => [...eventsWithParentIds, ...pre])
|
||||
setEventMap((pre) => ({ ...pre, ...eventMap }))
|
||||
setReplies((pre) => [...processedEvents, ...pre])
|
||||
}
|
||||
if (sortedEvents.length > 0) {
|
||||
setUntil(sortedEvents[0].created_at - 1)
|
||||
@@ -50,8 +47,39 @@ export default function ReplyNoteList({ event, className }: { event: Event; clas
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
updateNoteReplyCount(event.id, eventsWithParentIds.length)
|
||||
}, [eventsWithParentIds])
|
||||
updateNoteReplyCount(event.id, replies.length)
|
||||
|
||||
const replyMap: Record<string, { event: Event; level: number; parent?: Event } | undefined> = {}
|
||||
for (const reply of replies) {
|
||||
const parentReplyTag = reply.tags.find(isReplyETag)
|
||||
if (parentReplyTag) {
|
||||
const parentReplyInfo = replyMap[parentReplyTag[1]]
|
||||
const level = parentReplyInfo ? parentReplyInfo.level + 1 : 1
|
||||
replyMap[reply.id] = { event: reply, level, parent: parentReplyInfo?.event }
|
||||
continue
|
||||
}
|
||||
|
||||
const rootReplyTag = reply.tags.find(isRootETag)
|
||||
if (rootReplyTag) {
|
||||
replyMap[reply.id] = { event: reply, level: 1 }
|
||||
continue
|
||||
}
|
||||
|
||||
let level = 0
|
||||
let parent: Event | undefined
|
||||
for (const [tagName, tagValue] of reply.tags) {
|
||||
if (tagName === 'e') {
|
||||
const info = replyMap[tagValue]
|
||||
if (info && info.level > level) {
|
||||
level = info.level
|
||||
parent = info.event
|
||||
}
|
||||
}
|
||||
}
|
||||
replyMap[reply.id] = { event: reply, level: level + 1, parent }
|
||||
}
|
||||
setReplyMap(replyMap)
|
||||
}, [replies])
|
||||
|
||||
const onClickParent = (eventId: string) => {
|
||||
const ref = replyRefs.current[eventId]
|
||||
@@ -72,20 +100,23 @@ export default function ReplyNoteList({ event, className }: { event: Event; clas
|
||||
>
|
||||
{loading ? 'loading...' : hasMore ? 'load more older replies' : null}
|
||||
</div>
|
||||
{eventsWithParentIds.length > 0 && (loading || hasMore) && <Separator className="my-4" />}
|
||||
{replies.length > 0 && (loading || hasMore) && <Separator className="my-4" />}
|
||||
<div className={cn('mb-4', className)}>
|
||||
{eventsWithParentIds.map(([event, parentEventId], index) => (
|
||||
<div ref={(el) => (replyRefs.current[event.id] = el)} key={index}>
|
||||
<ReplyNote
|
||||
event={event}
|
||||
parentEvent={parentEventId ? eventMap[parentEventId] : undefined}
|
||||
onClickParent={onClickParent}
|
||||
highlight={highlightReplyId === event.id}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{replies.map((reply, index) => {
|
||||
const info = replyMap[reply.id]
|
||||
return (
|
||||
<div ref={(el) => (replyRefs.current[reply.id] = el)} key={index}>
|
||||
<ReplyNote
|
||||
event={reply}
|
||||
parentEvent={info?.parent}
|
||||
onClickParent={onClickParent}
|
||||
highlight={highlightReplyId === reply.id}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{eventsWithParentIds.length === 0 && !loading && !hasMore && (
|
||||
{replies.length === 0 && !loading && !hasMore && (
|
||||
<div className="text-sm text-center text-muted-foreground">no replies</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -41,15 +41,14 @@ export async function createShortTextNoteDraftEvent(
|
||||
content: string,
|
||||
parentEvent?: Event
|
||||
): Promise<TDraftEvent> {
|
||||
const { pubkeys, eventIds, rootEventId, parentEventId } = await extractMentions(
|
||||
content,
|
||||
parentEvent
|
||||
)
|
||||
const { pubkeys, otherRelatedEventIds, quoteEventIds, rootEventId, parentEventId } =
|
||||
await extractMentions(content, parentEvent)
|
||||
const hashtags = extractHashtags(content)
|
||||
|
||||
const tags = pubkeys
|
||||
.map((pubkey) => ['p', pubkey])
|
||||
.concat(eventIds.map((eventId) => ['q', eventId])) // TODO: ["q", <event-id>, <relay-url>, <pubkey>]
|
||||
.concat(otherRelatedEventIds.map((eventId) => ['e', eventId]))
|
||||
.concat(quoteEventIds.map((eventId) => ['q', eventId]))
|
||||
.concat(hashtags.map((hashtag) => ['t', hashtag]))
|
||||
.concat([['client', 'jumble']])
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import client from '@renderer/services/client.service'
|
||||
import { Event, kinds, nip19 } from 'nostr-tools'
|
||||
import { replyETag, rootETag, tagNameEquals } from './tag'
|
||||
import { isReplyETag, isRootETag, tagNameEquals } from './tag'
|
||||
|
||||
export function isNsfwEvent(event: Event) {
|
||||
return event.tags.some(
|
||||
@@ -10,15 +10,28 @@ export function isNsfwEvent(event: Event) {
|
||||
}
|
||||
|
||||
export function isReplyNoteEvent(event: Event) {
|
||||
return event.kind === kinds.ShortTextNote && event.tags.some(rootETag)
|
||||
if (event.kind !== kinds.ShortTextNote) return false
|
||||
|
||||
let hasETag = false
|
||||
let hasMarker = false
|
||||
for (const [tagName, , , marker] of event.tags) {
|
||||
if (tagName !== 'e') continue
|
||||
hasETag = true
|
||||
|
||||
if (!marker) continue
|
||||
hasMarker = true
|
||||
|
||||
if (['root', 'reply'].includes(marker)) return true
|
||||
}
|
||||
return hasETag && !hasMarker
|
||||
}
|
||||
|
||||
export function getParentEventId(event?: Event) {
|
||||
return event?.tags.find(replyETag)?.[1]
|
||||
return event?.tags.find(isReplyETag)?.[1]
|
||||
}
|
||||
|
||||
export function getRootEventId(event?: Event) {
|
||||
return event?.tags.find(rootETag)?.[1]
|
||||
return event?.tags.find(isRootETag)?.[1]
|
||||
}
|
||||
|
||||
export function isReplaceable(kind: number) {
|
||||
@@ -40,7 +53,8 @@ export function getSharableEventId(event: Event) {
|
||||
|
||||
export async function extractMentions(content: string, parentEvent?: Event) {
|
||||
const pubkeySet = new Set<string>()
|
||||
const eventIdSet = new Set<string>()
|
||||
const relatedEventIdSet = new Set<string>()
|
||||
const quoteEventIdSet = new Set<string>()
|
||||
let rootEventId: string | undefined
|
||||
let parentEventId: string | undefined
|
||||
const matches = content.match(
|
||||
@@ -59,6 +73,7 @@ export async function extractMentions(content: string, parentEvent?: Event) {
|
||||
const event = await client.fetchEventByBench32Id(id)
|
||||
if (event) {
|
||||
pubkeySet.add(event.pubkey)
|
||||
quoteEventIdSet.add(event.id)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -67,29 +82,31 @@ export async function extractMentions(content: string, parentEvent?: Event) {
|
||||
}
|
||||
|
||||
if (parentEvent) {
|
||||
relatedEventIdSet.add(parentEvent.id)
|
||||
pubkeySet.add(parentEvent.pubkey)
|
||||
parentEvent.tags.forEach((tag) => {
|
||||
if (tagNameEquals('p')(tag)) {
|
||||
pubkeySet.add(tag[1])
|
||||
} else if (rootETag(tag)) {
|
||||
} else if (isRootETag(tag)) {
|
||||
rootEventId = tag[1]
|
||||
} else if (tagNameEquals('e')(tag)) {
|
||||
eventIdSet.add(tag[1])
|
||||
relatedEventIdSet.add(tag[1])
|
||||
}
|
||||
})
|
||||
if (rootEventId) {
|
||||
if (rootEventId || isReplyNoteEvent(parentEvent)) {
|
||||
parentEventId = parentEvent.id
|
||||
} else {
|
||||
rootEventId = parentEvent.id
|
||||
}
|
||||
}
|
||||
|
||||
if (rootEventId) eventIdSet.delete(rootEventId)
|
||||
if (parentEventId) eventIdSet.delete(parentEventId)
|
||||
if (rootEventId) relatedEventIdSet.delete(rootEventId)
|
||||
if (parentEventId) relatedEventIdSet.delete(parentEventId)
|
||||
|
||||
return {
|
||||
pubkeys: Array.from(pubkeySet),
|
||||
eventIds: Array.from(eventIdSet),
|
||||
otherRelatedEventIds: Array.from(relatedEventIdSet),
|
||||
quoteEventIds: Array.from(quoteEventIdSet),
|
||||
rootEventId,
|
||||
parentEventId
|
||||
}
|
||||
|
||||
@@ -2,10 +2,14 @@ export function tagNameEquals(tagName: string) {
|
||||
return (tag: string[]) => tag[0] === tagName
|
||||
}
|
||||
|
||||
export function replyETag([tagName, , , alt]: string[]) {
|
||||
return tagName === 'e' && alt === 'reply'
|
||||
export function isReplyETag([tagName, , , marker]: string[]) {
|
||||
return tagName === 'e' && marker === 'reply'
|
||||
}
|
||||
|
||||
export function rootETag([tagName, , , alt]: string[]) {
|
||||
return tagName === 'e' && alt === 'root'
|
||||
export function isRootETag([tagName, , , marker]: string[]) {
|
||||
return tagName === 'e' && marker === 'root'
|
||||
}
|
||||
|
||||
export function isMentionETag([tagName, , , marker]: string[]) {
|
||||
return tagName === 'e' && marker === 'mention'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user