refactor: replies
This commit is contained in:
parent
25233344e7
commit
63c9713ea8
5 changed files with 166 additions and 138 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { isMentioningMutedUsers } from '@/lib/event'
|
||||
import { getEventKey, isMentioningMutedUsers } from '@/lib/event'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||
import { useMuteList } from '@/providers/MuteListProvider'
|
||||
|
|
@ -20,27 +20,35 @@ export default function ReplyButton({ event }: { event: Event }) {
|
|||
const { mutePubkeySet } = useMuteList()
|
||||
const { hideContentMentioningMutedUsers } = useContentPolicy()
|
||||
const { replyCount, hasReplied } = useMemo(() => {
|
||||
const key = getEventKey(event)
|
||||
const hasReplied = pubkey
|
||||
? repliesMap.get(event.id)?.events.some((evt) => evt.pubkey === pubkey)
|
||||
? repliesMap.get(key)?.events.some((evt) => evt.pubkey === pubkey)
|
||||
: false
|
||||
|
||||
return {
|
||||
replyCount:
|
||||
repliesMap.get(event.id)?.events.filter((evt) => {
|
||||
if (hideUntrustedInteractions && !isUserTrusted(evt.pubkey)) {
|
||||
return false
|
||||
}
|
||||
if (mutePubkeySet.has(evt.pubkey)) {
|
||||
return false
|
||||
}
|
||||
if (hideContentMentioningMutedUsers && isMentioningMutedUsers(evt, mutePubkeySet)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}).length ?? 0,
|
||||
hasReplied
|
||||
let replyCount = 0
|
||||
const replies = [...(repliesMap.get(key)?.events || [])]
|
||||
while (replies.length > 0) {
|
||||
const reply = replies.pop()
|
||||
if (!reply) break
|
||||
|
||||
const replyKey = getEventKey(reply)
|
||||
const nestedReplies = repliesMap.get(replyKey)?.events ?? []
|
||||
replies.push(...nestedReplies)
|
||||
|
||||
if (hideUntrustedInteractions && !isUserTrusted(reply.pubkey)) {
|
||||
continue
|
||||
}
|
||||
if (mutePubkeySet.has(reply.pubkey)) {
|
||||
continue
|
||||
}
|
||||
if (hideContentMentioningMutedUsers && isMentioningMutedUsers(reply, mutePubkeySet)) {
|
||||
continue
|
||||
}
|
||||
replyCount++
|
||||
}
|
||||
}, [repliesMap, event.id, hideUntrustedInteractions])
|
||||
|
||||
return { replyCount, hasReplied }
|
||||
}, [repliesMap, event, hideUntrustedInteractions])
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
import { BIG_RELAY_URLS, ExtendedKind } from '@/constants'
|
||||
import {
|
||||
getParentETag,
|
||||
getEventKey,
|
||||
getEventKeyFromTag,
|
||||
getParentTag,
|
||||
getReplaceableCoordinateFromEvent,
|
||||
getRootATag,
|
||||
getRootETag,
|
||||
getRootEventHexId,
|
||||
getRootTag,
|
||||
isMentioningMutedUsers,
|
||||
isReplaceableEvent,
|
||||
isReplyNoteEvent
|
||||
} from '@/lib/event'
|
||||
import { toNote } from '@/lib/link'
|
||||
import { generateBech32IdFromETag, tagNameEquals } from '@/lib/tag'
|
||||
import { generateBech32IdFromATag, generateBech32IdFromETag, tagNameEquals } from '@/lib/tag'
|
||||
import { useSecondaryPage } from '@/PageManager'
|
||||
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||
import { useMuteList } from '@/providers/MuteListProvider'
|
||||
|
|
@ -40,23 +40,22 @@ export default function ReplyNoteList({ index, event }: { index?: number; event:
|
|||
const [rootInfo, setRootInfo] = useState<TRootInfo | undefined>(undefined)
|
||||
const { repliesMap, addReplies } = useReply()
|
||||
const replies = useMemo(() => {
|
||||
const replyIdSet = new Set<string>()
|
||||
const replyKeySet = new Set<string>()
|
||||
const replyEvents: NEvent[] = []
|
||||
const currentEventKey = isReplaceableEvent(event.kind)
|
||||
? getReplaceableCoordinateFromEvent(event)
|
||||
: event.id
|
||||
const currentEventKey = getEventKey(event)
|
||||
let parentEventKeys = [currentEventKey]
|
||||
while (parentEventKeys.length > 0) {
|
||||
const events = parentEventKeys.flatMap((id) => repliesMap.get(id)?.events || [])
|
||||
const events = parentEventKeys.flatMap((key) => repliesMap.get(key)?.events || [])
|
||||
events.forEach((evt) => {
|
||||
if (replyIdSet.has(evt.id)) return
|
||||
const key = getEventKey(evt)
|
||||
if (replyKeySet.has(key)) return
|
||||
if (mutePubkeySet.has(evt.pubkey)) return
|
||||
if (hideContentMentioningMutedUsers && isMentioningMutedUsers(evt, mutePubkeySet)) return
|
||||
|
||||
replyIdSet.add(evt.id)
|
||||
replyKeySet.add(key)
|
||||
replyEvents.push(evt)
|
||||
})
|
||||
parentEventKeys = events.map((evt) => evt.id)
|
||||
parentEventKeys = events.map((evt) => getEventKey(evt))
|
||||
}
|
||||
return replyEvents.sort((a, b) => a.created_at - b.created_at)
|
||||
}, [event.id, repliesMap])
|
||||
|
|
@ -64,7 +63,7 @@ export default function ReplyNoteList({ index, event }: { index?: number; event:
|
|||
const [until, setUntil] = useState<number | undefined>(undefined)
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [showCount, setShowCount] = useState(SHOW_COUNT)
|
||||
const [highlightReplyId, setHighlightReplyId] = useState<string | undefined>(undefined)
|
||||
const [highlightReplyKey, setHighlightReplyKey] = useState<string | undefined>(undefined)
|
||||
const replyRefs = useRef<Record<string, HTMLDivElement | null>>({})
|
||||
const bottomRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
|
|
@ -79,13 +78,14 @@ export default function ReplyNoteList({ index, event }: { index?: number; event:
|
|||
relay: client.getEventHint(event.id)
|
||||
}
|
||||
: { type: 'E', id: event.id, pubkey: event.pubkey }
|
||||
const rootETag = getRootETag(event)
|
||||
if (rootETag) {
|
||||
const [, rootEventHexId, , , rootEventPubkey] = rootETag
|
||||
|
||||
const rootTag = getRootTag(event)
|
||||
if (rootTag?.type === 'e') {
|
||||
const [, rootEventHexId, , , rootEventPubkey] = rootTag.tag
|
||||
if (rootEventHexId && rootEventPubkey) {
|
||||
root = { type: 'E', id: rootEventHexId, pubkey: rootEventPubkey }
|
||||
} else {
|
||||
const rootEventId = generateBech32IdFromETag(rootETag)
|
||||
const rootEventId = generateBech32IdFromETag(rootTag.tag)
|
||||
if (rootEventId) {
|
||||
const rootEvent = await client.fetchEvent(rootEventId)
|
||||
if (rootEvent) {
|
||||
|
|
@ -93,13 +93,11 @@ export default function ReplyNoteList({ index, event }: { index?: number; event:
|
|||
}
|
||||
}
|
||||
}
|
||||
} else if (event.kind === ExtendedKind.COMMENT) {
|
||||
const rootATag = getRootATag(event)
|
||||
if (rootATag) {
|
||||
const [, coordinate, relay] = rootATag
|
||||
const [, pubkey] = coordinate.split(':')
|
||||
root = { type: 'A', id: coordinate, eventId: event.id, pubkey, relay }
|
||||
}
|
||||
} else if (rootTag?.type === 'a') {
|
||||
const [, coordinate, relay] = rootTag.tag
|
||||
const [, pubkey] = coordinate.split(':')
|
||||
root = { type: 'A', id: coordinate, eventId: event.id, pubkey, relay }
|
||||
} else {
|
||||
const rootITag = event.tags.find(tagNameEquals('I'))
|
||||
if (rootITag) {
|
||||
root = { type: 'I', id: rootITag[1] }
|
||||
|
|
@ -110,27 +108,6 @@ export default function ReplyNoteList({ index, event }: { index?: number; event:
|
|||
fetchRootEvent()
|
||||
}, [event])
|
||||
|
||||
const onNewReply = useCallback((evt: NEvent) => {
|
||||
addReplies([evt])
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!rootInfo) return
|
||||
const handleEventPublished = (data: Event) => {
|
||||
const customEvent = data as CustomEvent<NEvent>
|
||||
const evt = customEvent.detail
|
||||
const rootId = getRootEventHexId(evt)
|
||||
if (rootId === rootInfo.id && isReplyNoteEvent(evt)) {
|
||||
onNewReply(evt)
|
||||
}
|
||||
}
|
||||
|
||||
client.addEventListener('newEvent', handleEventPublished)
|
||||
return () => {
|
||||
client.removeEventListener('newEvent', handleEventPublished)
|
||||
}
|
||||
}, [rootInfo, onNewReply])
|
||||
|
||||
useEffect(() => {
|
||||
if (loading || !rootInfo || currentIndex !== index) return
|
||||
|
||||
|
|
@ -222,7 +199,7 @@ export default function ReplyNoteList({ index, event }: { index?: number; event:
|
|||
return () => {
|
||||
promise.then((closer) => closer?.())
|
||||
}
|
||||
}, [rootInfo, currentIndex, index, onNewReply])
|
||||
}, [rootInfo, currentIndex, index])
|
||||
|
||||
useEffect(() => {
|
||||
if (replies.length === 0) {
|
||||
|
|
@ -269,16 +246,23 @@ export default function ReplyNoteList({ index, event }: { index?: number; event:
|
|||
setLoading(false)
|
||||
}, [loading, until, timelineKey])
|
||||
|
||||
const highlightReply = useCallback((eventId: string, scrollTo = true) => {
|
||||
const highlightReply = useCallback((key: string, eventId?: string, scrollTo = true) => {
|
||||
let found = false
|
||||
if (scrollTo) {
|
||||
const ref = replyRefs.current[eventId]
|
||||
const ref = replyRefs.current[key]
|
||||
if (ref) {
|
||||
found = true
|
||||
ref.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
||||
}
|
||||
}
|
||||
setHighlightReplyId(eventId)
|
||||
if (!found) {
|
||||
if (eventId) push(toNote(eventId))
|
||||
return
|
||||
}
|
||||
|
||||
setHighlightReplyKey(key)
|
||||
setTimeout(() => {
|
||||
setHighlightReplyId((pre) => (pre === eventId ? undefined : pre))
|
||||
setHighlightReplyKey((pre) => (pre === key ? undefined : pre))
|
||||
}, 1500)
|
||||
}, [])
|
||||
|
||||
|
|
@ -296,7 +280,8 @@ export default function ReplyNoteList({ index, event }: { index?: number; event:
|
|||
<div>
|
||||
{replies.slice(0, showCount).map((reply) => {
|
||||
if (hideUntrustedInteractions && !isUserTrusted(reply.pubkey)) {
|
||||
const repliesForThisReply = repliesMap.get(reply.id)
|
||||
const replyKey = getEventKey(reply)
|
||||
const repliesForThisReply = repliesMap.get(replyKey)
|
||||
// If the reply is not trusted and there are no trusted replies for this reply, skip rendering
|
||||
if (
|
||||
!repliesForThisReply ||
|
||||
|
|
@ -306,27 +291,29 @@ export default function ReplyNoteList({ index, event }: { index?: number; event:
|
|||
}
|
||||
}
|
||||
|
||||
const parentETag = getParentETag(reply)
|
||||
const parentEventHexId = parentETag?.[1]
|
||||
const parentEventId = parentETag ? generateBech32IdFromETag(parentETag) : undefined
|
||||
const rootEventKey = getEventKey(event)
|
||||
const currentReplyKey = getEventKey(reply)
|
||||
const parentTag = getParentTag(reply)
|
||||
const parentEventKey = parentTag ? getEventKeyFromTag(parentTag.tag) : undefined
|
||||
const parentEventId = parentTag
|
||||
? parentTag.type === 'e'
|
||||
? generateBech32IdFromETag(parentTag.tag)
|
||||
: generateBech32IdFromATag(parentTag.tag)
|
||||
: undefined
|
||||
return (
|
||||
<div
|
||||
ref={(el) => (replyRefs.current[reply.id] = el)}
|
||||
key={reply.id}
|
||||
ref={(el) => (replyRefs.current[currentReplyKey] = el)}
|
||||
key={currentReplyKey}
|
||||
className="scroll-mt-12"
|
||||
>
|
||||
<ReplyNote
|
||||
event={reply}
|
||||
parentEventId={event.id !== parentEventHexId ? parentEventId : undefined}
|
||||
parentEventId={rootEventKey !== parentEventKey ? parentEventId : undefined}
|
||||
onClickParent={() => {
|
||||
if (!parentEventHexId) return
|
||||
if (replies.every((r) => r.id !== parentEventHexId)) {
|
||||
push(toNote(parentEventId ?? parentEventHexId))
|
||||
return
|
||||
}
|
||||
highlightReply(parentEventHexId)
|
||||
if (!parentEventKey) return
|
||||
highlightReply(parentEventKey, parentEventId)
|
||||
}}
|
||||
highlight={highlightReplyId === reply.id}
|
||||
highlight={highlightReplyKey === currentReplyKey}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -31,12 +31,13 @@ export function isReplyNoteEvent(event: Event) {
|
|||
const cache = EVENT_IS_REPLY_NOTE_CACHE.get(event.id)
|
||||
if (cache !== undefined) return cache
|
||||
|
||||
const isReply = !!getParentETag(event) || !!getParentATag(event)
|
||||
const isReply = !!getParentTag(event)
|
||||
EVENT_IS_REPLY_NOTE_CACHE.set(event.id, isReply)
|
||||
return isReply
|
||||
}
|
||||
|
||||
export function isReplaceableEvent(kind: number) {
|
||||
if (isNaN(kind)) return false
|
||||
return kinds.isReplaceableKind(kind) || kinds.isAddressableKind(kind)
|
||||
}
|
||||
|
||||
|
|
@ -98,16 +99,32 @@ export function getParentEventHexId(event?: Event) {
|
|||
return tag?.[1]
|
||||
}
|
||||
|
||||
export function getParentBech32Id(event?: Event) {
|
||||
const eTag = getParentETag(event)
|
||||
if (!eTag) {
|
||||
const aTag = getParentATag(event)
|
||||
if (!aTag) return undefined
|
||||
export function getParentTag(event?: Event): { type: 'e' | 'a'; tag: string[] } | undefined {
|
||||
if (!event) return undefined
|
||||
|
||||
return generateBech32IdFromATag(aTag)
|
||||
if (event.kind === kinds.ShortTextNote) {
|
||||
const tag = getParentETag(event)
|
||||
return tag ? { type: 'e', tag } : undefined
|
||||
}
|
||||
|
||||
return generateBech32IdFromETag(eTag)
|
||||
// NIP-22
|
||||
const parentKindStr = event.tags.find(tagNameEquals('k'))?.[1]
|
||||
if (parentKindStr && isReplaceableEvent(parseInt(parentKindStr))) {
|
||||
const tag = getParentATag(event)
|
||||
return tag ? { type: 'a', tag } : undefined
|
||||
}
|
||||
|
||||
const tag = getParentETag(event)
|
||||
return tag ? { type: 'e', tag } : undefined
|
||||
}
|
||||
|
||||
export function getParentBech32Id(event?: Event) {
|
||||
const parentTag = getParentTag(event)
|
||||
if (!parentTag) return undefined
|
||||
|
||||
return parentTag.type === 'e'
|
||||
? generateBech32IdFromETag(parentTag.tag)
|
||||
: generateBech32IdFromATag(parentTag.tag)
|
||||
}
|
||||
|
||||
export function getRootETag(event?: Event) {
|
||||
|
|
@ -147,16 +164,42 @@ export function getRootEventHexId(event?: Event) {
|
|||
return tag?.[1]
|
||||
}
|
||||
|
||||
export function getRootBech32Id(event?: Event) {
|
||||
const eTag = getRootETag(event)
|
||||
if (!eTag) {
|
||||
const aTag = getRootATag(event)
|
||||
if (!aTag) return undefined
|
||||
export function getRootTag(event?: Event): { type: 'e' | 'a'; tag: string[] } | undefined {
|
||||
if (!event) return undefined
|
||||
|
||||
return generateBech32IdFromATag(aTag)
|
||||
if (event.kind === kinds.ShortTextNote) {
|
||||
const tag = getRootETag(event)
|
||||
return tag ? { type: 'e', tag } : undefined
|
||||
}
|
||||
|
||||
return generateBech32IdFromETag(eTag)
|
||||
// NIP-22
|
||||
const rootKindStr = event.tags.find(tagNameEquals('K'))?.[1]
|
||||
if (rootKindStr && isReplaceableEvent(parseInt(rootKindStr))) {
|
||||
const tag = getRootATag(event)
|
||||
return tag ? { type: 'a', tag } : undefined
|
||||
}
|
||||
|
||||
const tag = getRootETag(event)
|
||||
return tag ? { type: 'e', tag } : undefined
|
||||
}
|
||||
|
||||
export function getRootBech32Id(event?: Event) {
|
||||
const rootTag = getRootTag(event)
|
||||
if (!rootTag) return undefined
|
||||
|
||||
return rootTag.type === 'e'
|
||||
? generateBech32IdFromETag(rootTag.tag)
|
||||
: generateBech32IdFromATag(rootTag.tag)
|
||||
}
|
||||
|
||||
// For internal identification of events
|
||||
export function getEventKey(event: Event) {
|
||||
return isReplaceableEvent(event.kind) ? getReplaceableCoordinateFromEvent(event) : event.id
|
||||
}
|
||||
|
||||
// Only used for e, E, a, A tags
|
||||
export function getEventKeyFromTag([, tagValue]: (string | undefined)[]) {
|
||||
return tagValue
|
||||
}
|
||||
|
||||
export function getReplaceableCoordinate(kind: number, pubkey: string, d: string = '') {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,13 @@ import { Skeleton } from '@/components/ui/skeleton'
|
|||
import { ExtendedKind } from '@/constants'
|
||||
import { useFetchEvent } from '@/hooks'
|
||||
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
|
||||
import { getParentBech32Id, getParentETag, getRootBech32Id } from '@/lib/event'
|
||||
import {
|
||||
getEventKey,
|
||||
getEventKeyFromTag,
|
||||
getParentBech32Id,
|
||||
getParentTag,
|
||||
getRootBech32Id
|
||||
} from '@/lib/event'
|
||||
import { toNote, toNoteList } from '@/lib/link'
|
||||
import { tagNameEquals } from '@/lib/tag'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
|
@ -173,8 +179,10 @@ function ParentNote({
|
|||
}
|
||||
|
||||
function isConsecutive(rootEvent?: Event, parentEvent?: Event) {
|
||||
const eTag = getParentETag(parentEvent)
|
||||
if (!eTag) return false
|
||||
if (!rootEvent || !parentEvent) return false
|
||||
|
||||
return rootEvent?.id === eTag[1]
|
||||
const tag = getParentTag(parentEvent)
|
||||
if (!tag) return false
|
||||
|
||||
return getEventKey(rootEvent) === getEventKeyFromTag(tag.tag)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { getParentATag, getParentETag, getRootATag, getRootETag } from '@/lib/event'
|
||||
import { getEventKey, getEventKeyFromTag, getParentTag } from '@/lib/event'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { createContext, useCallback, useContext, useState } from 'react'
|
||||
|
||||
type TReplyContext = {
|
||||
repliesMap: Map<string, { events: Event[]; eventIdSet: Set<string> }>
|
||||
repliesMap: Map<string, { events: Event[]; eventKeySet: Set<string> }>
|
||||
addReplies: (replies: Event[]) => void
|
||||
}
|
||||
|
||||
|
|
@ -19,56 +19,38 @@ export const useReply = () => {
|
|||
|
||||
export function ReplyProvider({ children }: { children: React.ReactNode }) {
|
||||
const [repliesMap, setRepliesMap] = useState<
|
||||
Map<string, { events: Event[]; eventIdSet: Set<string> }>
|
||||
Map<string, { events: Event[]; eventKeySet: Set<string> }>
|
||||
>(new Map())
|
||||
|
||||
const addReplies = useCallback((replies: Event[]) => {
|
||||
const newReplyIdSet = new Set<string>()
|
||||
const newReplyKeySet = new Set<string>()
|
||||
const newReplyEventMap = new Map<string, Event[]>()
|
||||
replies.forEach((reply) => {
|
||||
if (newReplyIdSet.has(reply.id)) return
|
||||
newReplyIdSet.add(reply.id)
|
||||
const key = getEventKey(reply)
|
||||
if (newReplyKeySet.has(key)) return
|
||||
newReplyKeySet.add(key)
|
||||
|
||||
let rootId: string | undefined
|
||||
const rootETag = getRootETag(reply)
|
||||
if (rootETag) {
|
||||
rootId = rootETag[1]
|
||||
} else {
|
||||
const rootATag = getRootATag(reply)
|
||||
if (rootATag) {
|
||||
rootId = rootATag[1]
|
||||
const parentTag = getParentTag(reply)
|
||||
if (parentTag) {
|
||||
const parentKey = getEventKeyFromTag(parentTag.tag)
|
||||
if (parentKey) {
|
||||
newReplyEventMap.set(parentKey, [...(newReplyEventMap.get(parentKey) || []), reply])
|
||||
}
|
||||
}
|
||||
if (rootId) {
|
||||
newReplyEventMap.set(rootId, [...(newReplyEventMap.get(rootId) || []), reply])
|
||||
}
|
||||
|
||||
let parentId: string | undefined
|
||||
const parentETag = getParentETag(reply)
|
||||
if (parentETag) {
|
||||
parentId = parentETag[1]
|
||||
} else {
|
||||
const parentATag = getParentATag(reply)
|
||||
if (parentATag) {
|
||||
parentId = parentATag[1]
|
||||
}
|
||||
}
|
||||
if (parentId && parentId !== rootId) {
|
||||
newReplyEventMap.set(parentId, [...(newReplyEventMap.get(parentId) || []), reply])
|
||||
}
|
||||
})
|
||||
if (newReplyEventMap.size === 0) return
|
||||
|
||||
setRepliesMap((prev) => {
|
||||
for (const [id, newReplyEvents] of newReplyEventMap.entries()) {
|
||||
const replies = prev.get(id) || { events: [], eventIdSet: new Set() }
|
||||
for (const [key, newReplyEvents] of newReplyEventMap.entries()) {
|
||||
const replies = prev.get(key) || { events: [], eventKeySet: new Set() }
|
||||
newReplyEvents.forEach((reply) => {
|
||||
if (!replies.eventIdSet.has(reply.id)) {
|
||||
const key = getEventKey(reply)
|
||||
if (!replies.eventKeySet.has(key)) {
|
||||
replies.events.push(reply)
|
||||
replies.eventIdSet.add(reply.id)
|
||||
replies.eventKeySet.add(key)
|
||||
}
|
||||
})
|
||||
prev.set(id, replies)
|
||||
prev.set(key, replies)
|
||||
}
|
||||
return new Map(prev)
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue