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