From 32cc34582db0877f289399de7cd858b1c221f667 Mon Sep 17 00:00:00 2001 From: codytseng Date: Tue, 19 Nov 2024 16:09:15 +0800 Subject: [PATCH] fix: partial replies not being displayed --- .../src/components/ReplyNote/index.tsx | 7 +- .../src/components/ReplyNoteList/index.tsx | 79 +++++++++++++------ src/renderer/src/lib/draft-event.ts | 9 +-- src/renderer/src/lib/event.ts | 39 ++++++--- src/renderer/src/lib/tag.ts | 12 ++- 5 files changed, 101 insertions(+), 45 deletions(-) diff --git a/src/renderer/src/components/ReplyNote/index.tsx b/src/renderer/src/components/ReplyNote/index.tsx index 702f407..e5bf665 100644 --- a/src/renderer/src/components/ReplyNote/index.tsx +++ b/src/renderer/src/components/ReplyNote/index.tsx @@ -38,7 +38,12 @@ export default function ReplyNote({
{formatTimestamp(event.created_at)}
-
reply
+
setIsPostDialogOpen(true)} + > + reply +
diff --git a/src/renderer/src/components/ReplyNoteList/index.tsx b/src/renderer/src/components/ReplyNoteList/index.tsx index 15899f0..e87b5af 100644 --- a/src/renderer/src/components/ReplyNoteList/index.tsx +++ b/src/renderer/src/components/ReplyNoteList/index.tsx @@ -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>({}) + const [replies, setReplies] = useState([]) + const [replyMap, setReplyMap] = useState< + Record + >({}) const [until, setUntil] = useState(() => dayjs().unix()) const [loading, setLoading] = useState(false) const [hasMore, setHasMore] = useState(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 = {} - 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 = {} + 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} - {eventsWithParentIds.length > 0 && (loading || hasMore) && } + {replies.length > 0 && (loading || hasMore) && }
- {eventsWithParentIds.map(([event, parentEventId], index) => ( -
(replyRefs.current[event.id] = el)} key={index}> - -
- ))} + {replies.map((reply, index) => { + const info = replyMap[reply.id] + return ( +
(replyRefs.current[reply.id] = el)} key={index}> + +
+ ) + })}
- {eventsWithParentIds.length === 0 && !loading && !hasMore && ( + {replies.length === 0 && !loading && !hasMore && (
no replies
)} diff --git a/src/renderer/src/lib/draft-event.ts b/src/renderer/src/lib/draft-event.ts index 0eca7ff..944ea4c 100644 --- a/src/renderer/src/lib/draft-event.ts +++ b/src/renderer/src/lib/draft-event.ts @@ -41,15 +41,14 @@ export async function createShortTextNoteDraftEvent( content: string, parentEvent?: Event ): Promise { - 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", , , ] + .concat(otherRelatedEventIds.map((eventId) => ['e', eventId])) + .concat(quoteEventIds.map((eventId) => ['q', eventId])) .concat(hashtags.map((hashtag) => ['t', hashtag])) .concat([['client', 'jumble']]) diff --git a/src/renderer/src/lib/event.ts b/src/renderer/src/lib/event.ts index f2f0fba..c9f456e 100644 --- a/src/renderer/src/lib/event.ts +++ b/src/renderer/src/lib/event.ts @@ -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() - const eventIdSet = new Set() + const relatedEventIdSet = new Set() + const quoteEventIdSet = new Set() 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 } diff --git a/src/renderer/src/lib/tag.ts b/src/renderer/src/lib/tag.ts index e80bb20..e6499bd 100644 --- a/src/renderer/src/lib/tag.ts +++ b/src/renderer/src/lib/tag.ts @@ -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' }