diff --git a/src/components/Nip22ReplyNoteList/index.tsx b/src/components/Nip22ReplyNoteList/index.tsx deleted file mode 100644 index 6d64d06..0000000 --- a/src/components/Nip22ReplyNoteList/index.tsx +++ /dev/null @@ -1,205 +0,0 @@ -import { Separator } from '@/components/ui/separator' -import { BIG_RELAY_URLS, ExtendedKind } from '@/constants' -import { isCommentEvent, isProtectedEvent } from '@/lib/event' -import { generateEventId, tagNameEquals } from '@/lib/tag' -import { cn } from '@/lib/utils' -import { useNostr } from '@/providers/NostrProvider' -import client from '@/services/client.service' -import dayjs from 'dayjs' -import { Event as NEvent } from 'nostr-tools' -import { useCallback, useEffect, useRef, useState } from 'react' -import { useTranslation } from 'react-i18next' -import ReplyNote from '../ReplyNote' - -const LIMIT = 100 - -export default function Nip22ReplyNoteList({ - event, - className -}: { - event: NEvent - className?: string -}) { - const { t } = useTranslation() - const { pubkey, startLogin } = useNostr() - const [timelineKey, setTimelineKey] = useState(undefined) - const [until, setUntil] = useState(() => dayjs().unix()) - const [replies, setReplies] = useState([]) - const [replyMap, setReplyMap] = useState< - Record - >({}) - const [loading, setLoading] = useState(false) - const [highlightReplyId, setHighlightReplyId] = useState(undefined) - const replyRefs = useRef>({}) - const bottomRef = useRef(null) - - useEffect(() => { - const handleEventPublished = (data: Event) => { - const customEvent = data as CustomEvent - const evt = customEvent.detail - if ( - isCommentEvent(evt) && - evt.tags.some(([tagName, tagValue]) => tagName === 'E' && tagValue === event.id) - ) { - onNewReply(evt) - } - } - - client.addEventListener('eventPublished', handleEventPublished) - return () => { - client.removeEventListener('eventPublished', handleEventPublished) - } - }, [event]) - - useEffect(() => { - if (loading) return - - const init = async () => { - setLoading(true) - setReplies([]) - - try { - const relayList = await client.fetchRelayList(event.pubkey) - const relayUrls = relayList.read.concat(BIG_RELAY_URLS) - if (isProtectedEvent(event)) { - const seenOn = client.getSeenEventRelayUrls(event.id) - relayUrls.unshift(...seenOn) - } - const { closer, timelineKey } = await client.subscribeTimeline( - [ - { - urls: relayUrls.slice(0, 4), - filter: { - '#E': [event.id], - kinds: [ExtendedKind.COMMENT], - limit: LIMIT - } - } - ], - { - onEvents: (evts, eosed) => { - if (evts.length > 0) { - setReplies(evts.reverse()) - } - if (eosed) { - setLoading(false) - setUntil(evts.length >= LIMIT ? evts[evts.length - 1].created_at - 1 : undefined) - } - }, - onNew: (evt) => { - onNewReply(evt) - } - }, - { - startLogin - } - ) - setTimelineKey(timelineKey) - return closer - } catch { - setLoading(false) - } - return - } - - const promise = init() - return () => { - promise.then((closer) => closer?.()) - } - }, [event]) - - useEffect(() => { - const replyMap: Record = - {} - for (const reply of replies) { - const parentEventId = reply.tags.find(tagNameEquals('e'))?.[1] - if (parentEventId && parentEventId !== event.id) { - const parentReplyInfo = replyMap[parentEventId] - const level = parentReplyInfo ? parentReplyInfo.level + 1 : 1 - replyMap[reply.id] = { event: reply, level, parent: parentReplyInfo?.event } - continue - } - - replyMap[reply.id] = { event: reply, level: 1 } - continue - } - setReplyMap(replyMap) - }, [replies, event.id]) - - const loadMore = useCallback(async () => { - if (loading || !until || !timelineKey) return - - setLoading(true) - const events = await client.loadMoreTimeline(timelineKey, until, LIMIT) - const olderReplies = events.reverse() - if (olderReplies.length > 0) { - setReplies((pre) => [...olderReplies, ...pre]) - } - setUntil(events.length ? events[events.length - 1].created_at - 1 : undefined) - setLoading(false) - }, [loading, until, timelineKey]) - - const onNewReply = useCallback( - (evt: NEvent) => { - setReplies((pre) => { - if (pre.some((reply) => reply.id === evt.id)) return pre - return [...pre, evt] - }) - if (evt.pubkey === pubkey) { - setTimeout(() => { - if (bottomRef.current) { - bottomRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) - } - highlightReply(evt.id, false) - }, 100) - } - }, - [pubkey] - ) - - const highlightReply = useCallback((eventId: string, scrollTo = true) => { - if (scrollTo) { - const ref = replyRefs.current[eventId] - if (ref) { - ref.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) - } - } - setHighlightReplyId(eventId) - setTimeout(() => { - setHighlightReplyId((pre) => (pre === eventId ? undefined : pre)) - }, 1500) - }, []) - - return ( - <> - {(loading || until) && ( -
- {loading ? t('loading...') : t('load more older replies')} -
- )} - {replies.length > 0 && (loading || until) && } -
- {replies.map((reply) => { - const info = replyMap[reply.id] - return ( -
(replyRefs.current[reply.id] = el)} key={reply.id}> - info?.parent?.id && highlightReply(info?.parent?.id)} - highlight={highlightReplyId === reply.id} - /> -
- ) - })} -
- {replies.length === 0 && !loading && !until && ( -
{t('no replies')}
- )} -
- - ) -} diff --git a/src/components/ReplyNoteList/index.tsx b/src/components/ReplyNoteList/index.tsx index 10d65ae..96daee9 100644 --- a/src/components/ReplyNoteList/index.tsx +++ b/src/components/ReplyNoteList/index.tsx @@ -1,5 +1,5 @@ import { Separator } from '@/components/ui/separator' -import { BIG_RELAY_URLS } from '@/constants' +import { BIG_RELAY_URLS, ExtendedKind } from '@/constants' import { getParentEventTag, getRootEventHexId, @@ -10,7 +10,7 @@ import { generateEventIdFromETag } from '@/lib/tag' import { useSecondaryPage } from '@/PageManager' import { useReply } from '@/providers/ReplyProvider' import client from '@/services/client.service' -import { Event as NEvent, kinds } from 'nostr-tools' +import { Filter, Event as NEvent, kinds } from 'nostr-tools' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import ReplyNote from '../ReplyNote' @@ -109,17 +109,28 @@ export default function ReplyNoteList({ const relayUrls = relayList.read.concat(BIG_RELAY_URLS) const seenOn = client.getSeenEventRelayUrls(rootInfo.id) relayUrls.unshift(...seenOn) + + const filters: (Omit & { + limit: number + })[] = [ + { + '#e': [rootInfo.id], + kinds: [kinds.ShortTextNote], + limit: LIMIT + } + ] + if (event.kind !== kinds.ShortTextNote) { + filters.push({ + '#E': [rootInfo.id], + kinds: [ExtendedKind.COMMENT], + limit: LIMIT + }) + } const { closer, timelineKey } = await client.subscribeTimeline( - [ - { - urls: relayUrls.slice(0, 5), - filter: { - '#e': [rootInfo.id], - kinds: [kinds.ShortTextNote], - limit: LIMIT - } - } - ], + filters.map((filter) => ({ + urls: relayUrls.slice(0, 5), + filter + })), { onEvents: (evts, eosed) => { if (evts.length > 0) { diff --git a/src/lib/draft-event.ts b/src/lib/draft-event.ts index cd22ed9..522b982 100644 --- a/src/lib/draft-event.ts +++ b/src/lib/draft-event.ts @@ -177,11 +177,12 @@ export async function createCommentDraftEvent( const { quoteEventIds, rootEventId, - rootEventKind, - rootEventPubkey, + rootKind, + rootPubkey, + rootUrl, parentEventId, - parentEventKind, - parentEventPubkey + parentKind, + parentPubkey } = await extractCommentMentions(content, parentEvent) const hashtags = extractHashtags(content) @@ -194,17 +195,29 @@ export async function createCommentDraftEvent( tags.push(...generateImetaTags(images)) } - tags.push( - ...mentions.filter((pubkey) => pubkey !== parentEventPubkey).map((pubkey) => ['p', pubkey]) - ) + tags.push(...mentions.filter((pubkey) => pubkey !== parentPubkey).map((pubkey) => ['p', pubkey])) + + if (rootEventId) { + tags.push( + rootPubkey + ? ['E', rootEventId, client.getEventHint(rootEventId), rootPubkey] + : ['E', rootEventId, client.getEventHint(rootEventId)] + ) + } + if (rootPubkey) { + tags.push(['P', rootPubkey]) + } + if (rootKind) { + tags.push(['K', rootKind.toString()]) + } + if (rootUrl) { + tags.push(['I', rootUrl]) + } tags.push( ...[ - ['E', rootEventId, client.getEventHint(rootEventId), rootEventPubkey], - ['K', rootEventKind.toString()], - ['P', rootEventPubkey], - ['e', parentEventId, client.getEventHint(parentEventId), parentEventPubkey], - ['k', parentEventKind.toString()], - ['p', parentEventPubkey] + ['e', parentEventId, client.getEventHint(parentEventId), parentPubkey], + ['k', parentKind.toString()], + ['p', parentPubkey] ] ) diff --git a/src/lib/event.ts b/src/lib/event.ts index 591e509..9e8580e 100644 --- a/src/lib/event.ts +++ b/src/lib/event.ts @@ -26,6 +26,7 @@ export function isNsfwEvent(event: Event) { } export function isReplyNoteEvent(event: Event) { + if (event.kind === ExtendedKind.COMMENT) return true if (event.kind !== kinds.ShortTextNote) return false const cache = EVENT_IS_REPLY_NOTE_CACHE.get(event.id) @@ -64,7 +65,12 @@ export function isSupportedKind(kind: number) { } export function getParentEventTag(event?: Event) { - if (!event) return undefined + if (!event || ![kinds.ShortTextNote, ExtendedKind.COMMENT].includes(event.kind)) return undefined + + if (event.kind === ExtendedKind.COMMENT) { + return event.tags.find(tagNameEquals('e')) ?? event.tags.find(tagNameEquals('E')) + } + let tag = event.tags.find(isReplyETag) if (!tag) { const embeddedEventIds = extractEmbeddedEventIds(event) @@ -88,7 +94,12 @@ export function getParentEventId(event?: Event) { } export function getRootEventTag(event?: Event) { - if (!event) return undefined + if (!event || ![kinds.ShortTextNote, ExtendedKind.COMMENT].includes(event.kind)) return undefined + + if (event.kind === ExtendedKind.COMMENT) { + return event.tags.find(tagNameEquals('E')) + } + let tag = event.tags.find(isRootETag) if (!tag) { const embeddedEventIds = extractEmbeddedEventIds(event) @@ -338,12 +349,32 @@ export async function extractRelatedEventIds(content: string, parentEvent?: Even export async function extractCommentMentions(content: string, parentEvent: Event) { const quoteEventIds: string[] = [] - const rootEventId = parentEvent.tags.find(tagNameEquals('E'))?.[1] ?? parentEvent.id - const rootEventKind = parentEvent.tags.find(tagNameEquals('K'))?.[1] ?? parentEvent.kind - const rootEventPubkey = parentEvent.tags.find(tagNameEquals('P'))?.[1] ?? parentEvent.pubkey + let rootEventId = + parentEvent.kind === ExtendedKind.COMMENT + ? parentEvent.tags.find(tagNameEquals('E'))?.[1] + : parentEvent.id + let rootKind = + parentEvent.kind === ExtendedKind.COMMENT + ? parentEvent.tags.find(tagNameEquals('K'))?.[1] + : parentEvent.kind + let rootPubkey = + parentEvent.kind === ExtendedKind.COMMENT + ? parentEvent.tags.find(tagNameEquals('P'))?.[1] + : parentEvent.pubkey + const rootUrl = + parentEvent.kind === ExtendedKind.COMMENT + ? parentEvent.tags.find(tagNameEquals('I'))?.[1] + : undefined + + if (parentEvent.kind === ExtendedKind.COMMENT && !rootEventId) { + rootEventId = parentEvent.id + rootKind = parentEvent.kind + rootPubkey = parentEvent.pubkey + } + const parentEventId = parentEvent.id - const parentEventKind = parentEvent.kind - const parentEventPubkey = parentEvent.pubkey + const parentKind = parentEvent.kind + const parentPubkey = parentEvent.pubkey const addToSet = (arr: string[], item: string) => { if (!arr.includes(item)) arr.push(item) @@ -367,11 +398,12 @@ export async function extractCommentMentions(content: string, parentEvent: Event return { quoteEventIds, rootEventId, - rootEventKind, - rootEventPubkey, + rootKind, + rootPubkey, + rootUrl, parentEventId, - parentEventKind, - parentEventPubkey + parentKind, + parentPubkey } } diff --git a/src/pages/secondary/NotePage/index.tsx b/src/pages/secondary/NotePage/index.tsx index 48bf77a..4a3eb3b 100644 --- a/src/pages/secondary/NotePage/index.tsx +++ b/src/pages/secondary/NotePage/index.tsx @@ -1,6 +1,5 @@ import { useSecondaryPage } from '@/PageManager' import ContentPreview from '@/components/ContentPreview' -import Nip22ReplyNoteList from '@/components/Nip22ReplyNoteList' import Note from '@/components/Note' import NoteStats from '@/components/NoteStats' import PictureNote from '@/components/PictureNote' @@ -14,7 +13,6 @@ import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' import { getParentEventId, getRootEventId, isPictureEvent } from '@/lib/event' import { toNote } from '@/lib/link' import { useMuteList } from '@/providers/MuteListProvider' -import { kinds } from 'nostr-tools' import { forwardRef, useMemo } from 'react' import { useTranslation } from 'react-i18next' import NotFoundPage from '../NotFoundPage' @@ -22,14 +20,8 @@ import NotFoundPage from '../NotFoundPage' const NotePage = forwardRef(({ id, index }: { id?: string; index?: number }, ref) => { const { t } = useTranslation() const { event, isFetching } = useFetchEvent(id) - const parentEventId = useMemo( - () => (event?.kind === kinds.ShortTextNote ? getParentEventId(event) : undefined), - [event] - ) - const rootEventId = useMemo( - () => (event?.kind === kinds.ShortTextNote ? getRootEventId(event) : undefined), - [event] - ) + const parentEventId = useMemo(() => getParentEventId(event), [event]) + const rootEventId = useMemo(() => getRootEventId(event), [event]) if (!event && isFetching) { return ( @@ -65,7 +57,7 @@ const NotePage = forwardRef(({ id, index }: { id?: string; index?: number }, ref - + ) } @@ -86,11 +78,7 @@ const NotePage = forwardRef(({ id, index }: { id?: string; index?: number }, ref
- {[kinds.ShortTextNote, kinds.Highlights].includes(event.kind) ? ( - - ) : ( - - )} + ) }) diff --git a/src/services/client.service.ts b/src/services/client.service.ts index 5e28fca..5429c75 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -186,7 +186,7 @@ class ClientService extends EventTarget { ) { const newEventIdSet = new Set() const requestCount = subRequests.length - const threshold = Math.ceil(requestCount / 2) + const threshold = Math.floor(requestCount / 2) let eventIdSet = new Set() let events: NEvent[] = [] let eosedCount = 0