fix: partial replies not being displayed

This commit is contained in:
codytseng
2024-11-19 16:09:15 +08:00
parent 610dfbc9d8
commit 32cc34582d
5 changed files with 101 additions and 45 deletions

View File

@@ -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" />

View File

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

View File

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

View File

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

View File

@@ -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'
}