diff --git a/src/components/PostEditor/NormalPostContent.tsx b/src/components/PostEditor/NormalPostContent.tsx index 7849699a..290a55a9 100644 --- a/src/components/PostEditor/NormalPostContent.tsx +++ b/src/components/PostEditor/NormalPostContent.tsx @@ -2,11 +2,12 @@ import { Button } from '@/components/ui/button' import { useToast } from '@/hooks/use-toast' import { createCommentDraftEvent, createShortTextNoteDraftEvent } from '@/lib/draft-event' import { getRootEventTag } from '@/lib/event.ts' +import { generateEventIdFromETag } from '@/lib/tag.ts' import { useNostr } from '@/providers/NostrProvider' import client from '@/services/client.service' import postContentCache from '@/services/post-content-cache.service' import { ChevronDown, ImageUp, LoaderCircle } from 'lucide-react' -import { Event, kinds, nip19 } from 'nostr-tools' +import { Event, kinds } from 'nostr-tools' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import TextareaWithMentions from '../TextareaWithMentions.tsx' @@ -70,34 +71,28 @@ export default function NormalPostContent({ try { const additionalRelayUrls: string[] = [] if (parentEvent && !specifiedRelayUrls) { - const relayList = await client.fetchRelayList(parentEvent.pubkey) - additionalRelayUrls.push(...relayList.read.slice(0, 3)) const rootEventTag = getRootEventTag(parentEvent) if (rootEventTag) { - const [, rootEventId, rootEventRelay, , rootAuthor] = rootEventTag + const [, , , , rootAuthor] = rootEventTag if (rootAuthor) { if (rootAuthor !== parentEvent.pubkey) { const rootAuthorRelayList = await client.fetchRelayList(rootAuthor) - additionalRelayUrls.push(...rootAuthorRelayList.read.slice(0, 3)) + additionalRelayUrls.push(...rootAuthorRelayList.read.slice(0, 4)) } } else { - try { - const rootEvent = await client.fetchEvent( - nip19.neventEncode( - rootEventRelay - ? { id: rootEventId } - : { id: rootEventId, relays: [rootEventRelay] } - ) - ) + const rootEventId = generateEventIdFromETag(rootEventTag) + if (rootEventId) { + const rootEvent = await client.fetchEvent(rootEventId) + if (rootEvent && rootEvent.pubkey !== parentEvent.pubkey) { const rootAuthorRelayList = await client.fetchRelayList(rootEvent.pubkey) - additionalRelayUrls.push(...rootAuthorRelayList.read.slice(0, 3)) + additionalRelayUrls.push(...rootAuthorRelayList.read.slice(0, 4)) } - } catch { - // ignore } } } + const relayList = await client.fetchRelayList(parentEvent.pubkey) + additionalRelayUrls.push(...relayList.read.slice(0, 4)) } const draftEvent = parentEvent && parentEvent.kind !== kinds.ShortTextNote diff --git a/src/components/ReplyNoteList/index.tsx b/src/components/ReplyNoteList/index.tsx index 406b6ea0..bb1b452f 100644 --- a/src/components/ReplyNoteList/index.tsx +++ b/src/components/ReplyNoteList/index.tsx @@ -1,7 +1,12 @@ import { Separator } from '@/components/ui/separator' import { BIG_RELAY_URLS } from '@/constants' -import { isProtectedEvent, isReplyNoteEvent } from '@/lib/event' -import { isReplyETag, isRootETag } from '@/lib/tag' +import { + getParentEventHexId, + getRootEventHexId, + getRootEventTag, + isReplyNoteEvent +} from '@/lib/event' +import { generateEventIdFromETag } from '@/lib/tag' import { cn } from '@/lib/utils' import { useNostr } from '@/providers/NostrProvider' import { useNoteStats } from '@/providers/NoteStatsProvider' @@ -17,12 +22,14 @@ const LIMIT = 100 export default function ReplyNoteList({ event, className }: { event: NEvent; className?: string }) { const { t } = useTranslation() const { pubkey } = useNostr() + const [rootInfo, setRootInfo] = useState<{ id: string; pubkey: string } | undefined>(undefined) const [timelineKey, setTimelineKey] = useState(undefined) const [until, setUntil] = useState(() => dayjs().unix()) + const [events, setEvents] = useState([]) const [replies, setReplies] = useState([]) const [replyMap, setReplyMap] = useState< - Record - >({}) + Map + >(new Map()) const [loading, setLoading] = useState(false) const [highlightReplyId, setHighlightReplyId] = useState(undefined) const { updateNoteReplyCount } = useNoteStats() @@ -30,13 +37,35 @@ export default function ReplyNoteList({ event, className }: { event: NEvent; cla const bottomRef = useRef(null) useEffect(() => { + const fetchRootEvent = async () => { + let root = { id: event.id, pubkey: event.pubkey } + const rootEventTag = getRootEventTag(event) + if (rootEventTag) { + const [, rootEventHexId, , , rootEventPubkey] = rootEventTag + if (rootEventHexId && rootEventPubkey) { + root = { id: rootEventHexId, pubkey: rootEventPubkey } + } else { + const rootEventId = generateEventIdFromETag(rootEventTag) + if (rootEventId) { + const rootEvent = await client.fetchEvent(rootEventId) + if (rootEvent) { + root = { id: rootEvent.id, pubkey: rootEvent.pubkey } + } + } + } + } + setRootInfo(root) + } + fetchRootEvent() + }, [event]) + + useEffect(() => { + if (!rootInfo) return const handleEventPublished = (data: Event) => { const customEvent = data as CustomEvent const evt = customEvent.detail - if ( - isReplyNoteEvent(evt) && - evt.tags.some(([tagName, tagValue]) => tagName === 'e' && tagValue === event.id) - ) { + const rootId = getRootEventHexId(evt) + if (rootId === rootInfo.id) { onNewReply(evt) } } @@ -45,32 +74,30 @@ export default function ReplyNoteList({ event, className }: { event: NEvent; cla return () => { client.removeEventListener('eventPublished', handleEventPublished) } - }, [event]) + }, [rootInfo]) useEffect(() => { - if (loading) return + if (loading || !rootInfo) return const init = async () => { setLoading(true) - setReplies([]) + setEvents([]) try { - const relayList = await client.fetchRelayList(event.pubkey) + const relayList = await client.fetchRelayList(rootInfo.pubkey) const relayUrls = relayList.read.concat(BIG_RELAY_URLS) - if (isProtectedEvent(event)) { - const seenOn = client.getSeenEventRelayUrls(event.id) - relayUrls.unshift(...seenOn) - } + const seenOn = client.getSeenEventRelayUrls(rootInfo.id) + relayUrls.unshift(...seenOn) const { closer, timelineKey } = await client.subscribeTimeline( - relayUrls.slice(0, 4), + relayUrls.slice(0, 5), { - '#e': [event.id], + '#e': [rootInfo.id], kinds: [kinds.ShortTextNote], limit: LIMIT }, { onEvents: (evts, eosed) => { - setReplies(evts.filter((evt) => isReplyNoteEvent(evt)).reverse()) + setEvents(evts.filter((evt) => isReplyNoteEvent(evt)).reverse()) if (eosed) { setLoading(false) setUntil(evts.length >= LIMIT ? evts[evts.length - 1].created_at - 1 : undefined) @@ -94,52 +121,49 @@ export default function ReplyNoteList({ event, className }: { event: NEvent; cla return () => { promise.then((closer) => closer?.()) } - }, [event]) + }, [rootInfo]) useEffect(() => { - updateNoteReplyCount(event.id, replies.length) + const replies: NEvent[] = [] + const replyMap: Map = + new Map() + const rootEventId = getRootEventHexId(event) ?? event.id + const isRootEvent = rootEventId === event.id + for (const evt of events) { + if (evt.created_at < event.created_at) continue + + const parentEventId = getParentEventHexId(evt) + if (parentEventId) { + const parentReplyInfo = replyMap.get(parentEventId) + if (!parentReplyInfo && parentEventId !== event.id) continue - 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 } + replies.push(evt) + replyMap.set(evt.id, { event: evt, level, parent: parentReplyInfo?.event }) continue } - const rootReplyTag = reply.tags.find(isRootETag) - if (rootReplyTag) { - replyMap[reply.id] = { event: reply, level: 1 } - continue - } + if (!isRootEvent) continue - let level = 0 - let parent: NEvent | 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 } + replies.push(evt) + replyMap.set(evt.id, { event: evt, level: 1 }) } setReplyMap(replyMap) - }, [replies, event.id, updateNoteReplyCount]) + setReplies(replies) + updateNoteReplyCount(event.id, replies.length) + if (replies.length === 0) { + loadMore() + } + }, [events, event, updateNoteReplyCount]) const loadMore = useCallback(async () => { if (loading || !until || !timelineKey) return setLoading(true) const events = await client.loadMoreTimeline(timelineKey, until, LIMIT) - const olderReplies = events.filter((evt) => isReplyNoteEvent(evt)).reverse() - if (olderReplies.length > 0) { - setReplies((pre) => [...olderReplies, ...pre]) + const olderEvents = events.filter((evt) => isReplyNoteEvent(evt)).reverse() + if (olderEvents.length > 0) { + setEvents((pre) => [...olderEvents, ...pre]) } setUntil(events.length ? events[events.length - 1].created_at - 1 : undefined) setLoading(false) @@ -147,7 +171,7 @@ export default function ReplyNoteList({ event, className }: { event: NEvent; cla const onNewReply = useCallback( (evt: NEvent) => { - setReplies((pre) => { + setEvents((pre) => { if (pre.some((reply) => reply.id === evt.id)) return pre return [...pre, evt] }) @@ -192,7 +216,7 @@ export default function ReplyNoteList({ event, className }: { event: NEvent; cla {replies.length > 0 && (loading || until) && }
{replies.map((reply) => { - const info = replyMap[reply.id] + const info = replyMap.get(reply.id) return (
(replyRefs.current[reply.id] = el)} key={reply.id}> { - const { otherRelatedEventIds, quoteEventIds, rootEventId, parentEventId } = - await extractRelatedEventIds(content, options.parentEvent) + const { quoteEventIds, rootETag, parentETag } = await extractRelatedEventIds( + content, + options.parentEvent + ) const hashtags = extractHashtags(content) const tags = hashtags.map((hashtag) => ['t', hashtag]) @@ -77,14 +79,12 @@ export async function createShortTextNoteDraftEvent( tags.push(...quoteEventIds.map((eventId) => ['q', eventId, client.getEventHint(eventId)])) // e tags - if (rootEventId) { - tags.push(['e', rootEventId, client.getEventHint(rootEventId), 'root']) + if (rootETag.length) { + tags.push(rootETag) } - tags.push(...otherRelatedEventIds.map((eventId) => ['e', eventId, client.getEventHint(eventId)])) - - if (parentEventId) { - tags.push(['e', parentEventId, client.getEventHint(parentEventId), 'reply']) + if (parentETag.length) { + tags.push(parentETag) } // p tags diff --git a/src/lib/event.ts b/src/lib/event.ts index 699f8dd9..890e48b4 100644 --- a/src/lib/event.ts +++ b/src/lib/event.ts @@ -5,7 +5,13 @@ import { LRUCache } from 'lru-cache' import { Event, kinds, nip19 } from 'nostr-tools' import { getAmountFromInvoice, getLightningAddressFromProfile } from './lightning' import { formatPubkey } from './pubkey' -import { extractImageInfoFromTag, isReplyETag, isRootETag, tagNameEquals } from './tag' +import { + extractImageInfoFromTag, + generateEventIdFromETag, + isReplyETag, + isRootETag, + tagNameEquals +} from './tag' import { isWebsocketUrl, normalizeHttpUrl, normalizeUrl } from './url' const EVENT_EMBEDDED_EVENT_IDS_CACHE = new LRUCache({ max: 10000 }) @@ -56,23 +62,28 @@ export function isSupportedKind(kind: number) { return [kinds.ShortTextNote, PICTURE_EVENT_KIND].includes(kind) } -export function getParentEventId(event?: Event) { +export function getParentEventTag(event?: Event) { if (!event) return undefined let tag = event.tags.find(isReplyETag) if (!tag) { const embeddedEventIds = extractEmbeddedEventIds(event) tag = event.tags.findLast( - ([tagName, tagValue]) => tagName === 'e' && !embeddedEventIds.includes(tagValue) + ([tagName, tagValue]) => tagName === 'e' && !!tagValue && !embeddedEventIds.includes(tagValue) ) } + return tag +} + +export function getParentEventHexId(event?: Event) { + const tag = getParentEventTag(event) + return tag?.[1] +} + +export function getParentEventId(event?: Event) { + const tag = getParentEventTag(event) if (!tag) return undefined - try { - const [, id, relay, , author] = tag - return nip19.neventEncode({ id, relays: relay ? [relay] : undefined, author }) - } catch { - return undefined - } + return generateEventIdFromETag(tag) } export function getRootEventTag(event?: Event) { @@ -81,22 +92,22 @@ export function getRootEventTag(event?: Event) { if (!tag) { const embeddedEventIds = extractEmbeddedEventIds(event) tag = event.tags.find( - ([tagName, tagValue]) => tagName === 'e' && !embeddedEventIds.includes(tagValue) + ([tagName, tagValue]) => tagName === 'e' && !!tagValue && !embeddedEventIds.includes(tagValue) ) } return tag } +export function getRootEventHexId(event?: Event) { + const tag = getRootEventTag(event) + return tag?.[1] +} + export function getRootEventId(event?: Event) { const tag = getRootEventTag(event) if (!tag) return undefined - try { - const [, id, relay, , author] = tag - return nip19.neventEncode({ id, relays: relay ? [relay] : undefined, author }) - } catch { - return undefined - } + return generateEventIdFromETag(tag) } export function isReplaceable(kind: number) { @@ -231,21 +242,15 @@ export async function extractMentions(content: string, parentEvent?: Event) { } export async function extractRelatedEventIds(content: string, parentEvent?: Event) { - const relatedEventIds: string[] = [] const quoteEventIds: string[] = [] - let rootEventId: string | undefined - let parentEventId: string | undefined + let rootETag: string[] = [] + let parentETag: string[] = [] const matches = content.match(/nostr:(note1[a-z0-9]{58}|nevent1[a-z0-9]+)/g) const addToSet = (arr: string[], item: string) => { if (!arr.includes(item)) arr.push(item) } - const removeFromSet = (arr: string[], item: string) => { - const index = arr.indexOf(item) - if (index !== -1) arr.splice(index, 1) - } - for (const m of matches || []) { try { const id = m.split(':')[1] @@ -261,32 +266,48 @@ export async function extractRelatedEventIds(content: string, parentEvent?: Even } if (parentEvent) { - addToSet(relatedEventIds, parentEvent.id) - parentEvent.tags.forEach((tag) => { - if (isRootETag(tag)) { - rootEventId = tag[1] - } else if (tagNameEquals('e')(tag)) { - addToSet(relatedEventIds, tag[1]) + const rootEventTag = getRootEventTag(parentEvent) + if (rootEventTag) { + parentETag = [ + 'e', + parentEvent.id, + client.getEventHint(parentEvent.id), + 'reply', + parentEvent.pubkey + ] + + const [, rootEventHexId, hint, , rootEventPubkey] = rootEventTag + if (rootEventPubkey) { + rootETag = [ + 'e', + rootEventHexId, + hint ?? client.getEventHint(rootEventHexId), + 'root', + rootEventPubkey + ] + } else { + const rootEventId = generateEventIdFromETag(rootEventTag) + const rootEvent = rootEventId ? await client.fetchEvent(rootEventId) : undefined + rootETag = rootEvent + ? ['e', rootEvent.id, hint ?? client.getEventHint(rootEvent.id), 'root', rootEvent.pubkey] + : ['e', rootEventHexId, hint ?? client.getEventHint(rootEventHexId), 'root'] } - }) - if (rootEventId || isReplyNoteEvent(parentEvent)) { - parentEventId = parentEvent.id } else { - rootEventId = parentEvent.id + // reply to root event + rootETag = [ + 'e', + parentEvent.id, + client.getEventHint(parentEvent.id), + 'root', + parentEvent.pubkey + ] } } - if (rootEventId) { - removeFromSet(relatedEventIds, rootEventId) - } - if (parentEventId) { - removeFromSet(relatedEventIds, parentEventId) - } return { - otherRelatedEventIds: relatedEventIds, quoteEventIds, - rootEventId, - parentEventId + rootETag, + parentETag } } diff --git a/src/lib/tag.ts b/src/lib/tag.ts index bf3d8f2b..55f8893b 100644 --- a/src/lib/tag.ts +++ b/src/lib/tag.ts @@ -1,5 +1,6 @@ import { TImageInfo } from '@/types' import { isBlurhashValid } from 'blurhash' +import { nip19 } from 'nostr-tools' import { isValidPubkey } from './pubkey' export function tagNameEquals(tagName: string) { @@ -18,6 +19,15 @@ export function isMentionETag([tagName, , , marker]: string[]) { return tagName === 'e' && marker === 'mention' } +export function generateEventIdFromETag(tag: string[]) { + try { + const [, id, relay, , author] = tag + return nip19.neventEncode({ id, relays: relay ? [relay] : undefined, author }) + } catch { + return undefined + } +} + export function extractImageInfoFromTag(tag: string[]): TImageInfo | null { if (tag[0] !== 'imeta') return null const urlItem = tag.find((item) => item.startsWith('url ')) diff --git a/src/services/client.service.ts b/src/services/client.service.ts index 481cd1f2..24166645 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -17,7 +17,6 @@ import { SimplePool, VerifiedEvent } from 'nostr-tools' -import { SubscribeManyParams } from 'nostr-tools/abstract-pool' import { AbstractRelay } from 'nostr-tools/abstract-relay' import indexedDb from './indexed-db.service' @@ -185,11 +184,127 @@ class ClientService extends EventTarget { // eslint-disable-next-line @typescript-eslint/no-this-alias const that = this - const _knownIds = new Set() let events: NEvent[] = [] + let eosed = false + const subCloser = this.subscribe(relays, since ? { ...filter, since } : filter, { + startLogin, + onevent: (evt: NEvent) => { + that.eventDataLoader.prime(evt.id, Promise.resolve(evt)) + // not eosed yet, push to events + if (!eosed) { + return events.push(evt) + } + // eosed, (algo relay feeds) no need to sort and cache + if (!needSort) { + return onNew(evt) + } + + const timeline = that.timelines[key] + if (!timeline || !timeline.refs.length) { + return onNew(evt) + } + // the event is newer than the first ref, insert it to the front + if (evt.created_at > timeline.refs[0][1]) { + onNew(evt) + return timeline.refs.unshift([evt.id, evt.created_at]) + } + + let idx = 0 + for (const ref of timeline.refs) { + if (evt.created_at > ref[1] || (evt.created_at === ref[1] && evt.id < ref[0])) { + break + } + // the event is already in the cache + if (evt.created_at === ref[1] && evt.id === ref[0]) { + return + } + idx++ + } + // the event is too old, ignore it + if (idx >= timeline.refs.length) return + + // insert the event to the right position + timeline.refs.splice(idx, 0, [evt.id, evt.created_at]) + }, + oneose: (_eosed) => { + eosed = _eosed + // (algo feeds) no need to sort and cache + if (!needSort) { + return onEvents([...events], eosed) + } + if (!eosed) { + events = events.sort((a, b) => b.created_at - a.created_at).slice(0, filter.limit) + return onEvents([...events.concat(cachedEvents)], false) + } + + events = events.sort((a, b) => b.created_at - a.created_at).slice(0, filter.limit) + const timeline = that.timelines[key] + // no cache yet + if (!timeline || !timeline.refs.length) { + that.timelines[key] = { + refs: events.map((evt) => [evt.id, evt.created_at]), + filter, + urls + } + return onEvents([...events], true) + } + + const newEvents = events.filter((evt) => { + const firstRef = timeline.refs[0] + return ( + evt.created_at > firstRef[1] || (evt.created_at === firstRef[1] && evt.id < firstRef[0]) + ) + }) + const newRefs = newEvents.map((evt) => [evt.id, evt.created_at] as TTimelineRef) + + if (newRefs.length >= filter.limit) { + // if new refs are more than limit, means old refs are too old, replace them + timeline.refs = newRefs + onEvents([...newEvents], true) + } else { + // merge new refs with old refs + timeline.refs = newRefs.concat(timeline.refs) + onEvents([...newEvents.concat(cachedEvents)], true) + } + } + }) + + return { + timelineKey: key, + closer: () => { + onEvents = () => {} + onNew = () => {} + subCloser.close() + } + } + } + + subscribe( + urls: string[], + filter: Filter | Filter[], + { + onevent, + oneose, + onclose, + startLogin + }: { + onevent?: (evt: NEvent) => void + oneose?: (eosed: boolean) => void + onclose?: (reasons: string[]) => void + startLogin?: () => void + } + ) { + const relays = Array.from(new Set(urls)) + const filters = Array.isArray(filter) ? filter : [filter] + + // eslint-disable-next-line @typescript-eslint/no-this-alias + const that = this + const _knownIds = new Set() let startedCount = 0 let eosedCount = 0 let eosed = false + let closedCount = 0 + const closeReasons: string[] = [] const subPromises = relays.map(async (url) => { const relay = await this.pool.ensureRelay(url) let hasAuthed = false @@ -198,7 +313,7 @@ class ClientService extends EventTarget { function startSub() { startedCount++ - return relay.subscribe([since ? { ...filter, since } : filter], { + return relay.subscribe(filters, { receivedEvent: (relay, id) => { that.trackEventSeenOn(id, relay) }, @@ -211,45 +326,24 @@ class ClientService extends EventTarget { return false }, onevent: (evt: NEvent) => { - that.eventDataLoader.prime(evt.id, Promise.resolve(evt)) - // not eosed yet, push to events - if (eosedCount < startedCount) { - return events.push(evt) - } - // eosed, (algo relay feeds) no need to sort and cache - if (!needSort) { - return onNew(evt) - } + onevent?.(evt) + }, + oneose: () => { + if (eosed) return + eosedCount++ + eosed = eosedCount >= startedCount - const timeline = that.timelines[key] - if (!timeline || !timeline.refs.length) { - return onNew(evt) - } - // the event is newer than the first ref, insert it to the front - if (evt.created_at > timeline.refs[0][1]) { - onNew(evt) - return timeline.refs.unshift([evt.id, evt.created_at]) - } - - let idx = 0 - for (const ref of timeline.refs) { - if (evt.created_at > ref[1] || (evt.created_at === ref[1] && evt.id < ref[0])) { - break - } - // the event is already in the cache - if (evt.created_at === ref[1] && evt.id === ref[0]) { - return - } - idx++ - } - // the event is too old, ignore it - if (idx >= timeline.refs.length) return - - // insert the event to the right position - timeline.refs.splice(idx, 0, [evt.id, evt.created_at]) + oneose?.(eosed) }, onclose: (reason: string) => { - if (!reason.startsWith('auth-required:')) return + if (!reason.startsWith('auth-required:')) { + closedCount++ + closeReasons.push(reason) + if (closedCount >= startedCount) { + onclose?.(closeReasons) + } + return + } if (hasAuthed) return if (that.signer) { @@ -274,61 +368,13 @@ class ClientService extends EventTarget { startLogin() } }, - oneose: () => { - if (eosed) return - eosedCount++ - eosed = eosedCount >= startedCount - - // (algo feeds) no need to sort and cache - if (!needSort) { - return onEvents([...events], eosed) - } - if (!eosed) { - events = events.sort((a, b) => b.created_at - a.created_at).slice(0, filter.limit) - return onEvents([...events.concat(cachedEvents)], false) - } - - events = events.sort((a, b) => b.created_at - a.created_at).slice(0, filter.limit) - const timeline = that.timelines[key] - // no cache yet - if (!timeline || !timeline.refs.length) { - that.timelines[key] = { - refs: events.map((evt) => [evt.id, evt.created_at]), - filter, - urls - } - return onEvents([...events], true) - } - - const newEvents = events.filter((evt) => { - const firstRef = timeline.refs[0] - return ( - evt.created_at > firstRef[1] || - (evt.created_at === firstRef[1] && evt.id < firstRef[0]) - ) - }) - const newRefs = newEvents.map((evt) => [evt.id, evt.created_at] as TTimelineRef) - - if (newRefs.length >= filter.limit) { - // if new refs are more than limit, means old refs are too old, replace them - timeline.refs = newRefs - onEvents([...newEvents], true) - } else { - // merge new refs with old refs - timeline.refs = newRefs.concat(timeline.refs) - onEvents([...newEvents.concat(cachedEvents)], true) - } - }, eoseTimeout: 10000 // 10s }) } }) return { - timelineKey: key, - closer: () => { - onEvents = () => {} - onNew = () => {} + close: () => { subPromises.forEach((subPromise) => { subPromise .then((sub) => { @@ -342,12 +388,6 @@ class ClientService extends EventTarget { } } - subscribe(urls: string[], filter: Filter | Filter[], params: SubscribeManyParams) { - const relays = Array.from(new Set(urls)) - const filters = Array.isArray(filter) ? filter : [filter] - return this.pool.subscribeMany(relays, filters, params) - } - private async query(urls: string[], filter: Filter | Filter[], onevent?: (evt: NEvent) => void) { const relays = Array.from(new Set(urls)) const filters = Array.isArray(filter) ? filter : [filter] @@ -708,6 +748,7 @@ class ClientService extends EventTarget { private async _fetchEvent(id: string): Promise { let filter: Filter | undefined let relays: string[] = [] + let author: string | undefined if (/^[0-9a-f]{64}$/.test(id)) { filter = { ids: [id] } } else { @@ -719,6 +760,7 @@ class ClientService extends EventTarget { case 'nevent': filter = { ids: [data.id] } if (data.relays) relays = data.relays + if (data.author) author = data.author break case 'naddr': filter = { @@ -726,6 +768,7 @@ class ClientService extends EventTarget { kinds: [data.kind], limit: 1 } + author = data.pubkey if (data.identifier) { filter['#d'] = [data.identifier] } @@ -740,6 +783,10 @@ class ClientService extends EventTarget { if (filter.ids) { event = await this.fetchEventById(relays, filter.ids[0]) } else { + if (author) { + const relayList = await this.fetchRelayList(author) + relays.push(...relayList.write.slice(0, 4)) + } event = await this.tryHarderToFetchEvent(relays, filter) }