From 8f4b50dff9d5767862d1d0a9e2b5148993de86ad Mon Sep 17 00:00:00 2001 From: Silberengel Date: Tue, 5 May 2026 23:50:40 +0200 Subject: [PATCH] bug-fixes --- src/components/ReplyNoteList/index.tsx | 96 +++++++++++++++----------- src/constants.ts | 7 ++ src/services/client-query.service.ts | 18 +++++ 3 files changed, 80 insertions(+), 41 deletions(-) diff --git a/src/components/ReplyNoteList/index.tsx b/src/components/ReplyNoteList/index.tsx index 39d5e32e..87a12a39 100644 --- a/src/components/ReplyNoteList/index.tsx +++ b/src/components/ReplyNoteList/index.tsx @@ -78,6 +78,8 @@ type TRootInfo = const LIMIT = 200 const SHOW_COUNT = 10 const MAX_KINDS_PER_THREAD_REQ_FILTER = 4 +/** Some relays cap `#e` array length; chunk parent-id batches for nested-thread REQs. */ +const MAX_PARENT_IDS_PER_NESTED_REQ = 64 function chunkKindsForThreadReq(list: readonly number[], size = MAX_KINDS_PER_THREAD_REQ_FILTER): number[][] { const out: number[][] = [] @@ -1294,19 +1296,24 @@ function ReplyNoteList({ .filter((evt) => commentKinds.includes(evt.kind)) .map((evt) => evt.id) if (parentIds.length > 0) { - const nestedFilters: Filter[] = [ - { '#e': parentIds, kinds: commentKinds, limit: LIMIT } - ] - const nestedReplies = await queryService.fetchEvents(finalRelayUrls, nestedFilters, { - onevent: (evt: NEvent) => { - if (fetchGeneration !== replyFetchGenRef.current) return - if (shouldHideThreadResponseEvent(evt, mutePubkeySet, hideContentMentioningMutedUsers)) - return - addReplies([evt]) - } - }) - if (fetchGeneration !== replyFetchGenRef.current) return - const validNested = nestedReplies.filter( + const nestedAccum: NEvent[] = [] + for (let off = 0; off < parentIds.length; off += MAX_PARENT_IDS_PER_NESTED_REQ) { + const idChunk = parentIds.slice(off, off + MAX_PARENT_IDS_PER_NESTED_REQ) + const nestedFilters: Filter[] = [ + { '#e': idChunk, kinds: commentKinds, limit: LIMIT } + ] + const nestedReplies = await queryService.fetchEvents(finalRelayUrls, nestedFilters, { + onevent: (evt: NEvent) => { + if (fetchGeneration !== replyFetchGenRef.current) return + if (shouldHideThreadResponseEvent(evt, mutePubkeySet, hideContentMentioningMutedUsers)) + return + addReplies([evt]) + } + }) + if (fetchGeneration !== replyFetchGenRef.current) return + nestedAccum.push(...nestedReplies) + } + const validNested = nestedAccum.filter( (evt) => !shouldHideThreadResponseEvent(evt, mutePubkeySet, hideContentMentioningMutedUsers) ) @@ -1318,41 +1325,48 @@ function ReplyNoteList({ } } - // Second pass for kind-11 discussions: nested 1111/1 chains are keyed under parent ids in - // ReplyProvider; fetching #e:[comment-id] fills gaps the root-scoped REQ can miss. + // Second pass for discussions, plain kind-1 threads, and replaceable (longform/wiki) roots: + // nested 1 / 1111 / 1244 often tag only the parent's #e; root-scoped REQ misses them (same + // idea as URL-thread #I follow-up above). if ( - event.kind === ExtendedKind.DISCUSSION && - rootInfo.type === 'E' && - regularReplies.length > 0 + regularReplies.length > 0 && + ((rootInfo.type === 'E' && + (event.kind === ExtendedKind.DISCUSSION || event.kind === kinds.ShortTextNote)) || + rootInfo.type === 'A') ) { - const commentKinds = [ + const commentKindsNested = [ ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT, kinds.ShortTextNote ] - const parentIds = regularReplies - .filter((evt) => commentKinds.includes(evt.kind)) + const parentIdsNested = regularReplies + .filter((evt) => commentKindsNested.includes(evt.kind)) .map((evt) => evt.id) - if (parentIds.length > 0) { - const nestedFilters: Filter[] = [ - { '#e': parentIds, kinds: commentKinds, limit: LIMIT }, - { - '#E': parentIds, - kinds: [ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT], - limit: LIMIT - } - ] - const nestedReplies = await queryService.fetchEvents(finalRelayUrls, nestedFilters, { - onevent: (evt: NEvent) => { - if (fetchGeneration !== replyFetchGenRef.current) return - if (shouldHideThreadResponseEvent(evt, mutePubkeySet, hideContentMentioningMutedUsers)) - return - if (!replyMatchesThreadForList(evt, event, rootInfo, isDiscussionRoot)) return - addReplies([evt]) - } - }) - if (fetchGeneration !== replyFetchGenRef.current) return - const validNested = nestedReplies.filter( + if (parentIdsNested.length > 0) { + const nestedAccum: NEvent[] = [] + for (let off = 0; off < parentIdsNested.length; off += MAX_PARENT_IDS_PER_NESTED_REQ) { + const idChunk = parentIdsNested.slice(off, off + MAX_PARENT_IDS_PER_NESTED_REQ) + const nestedFilters: Filter[] = [ + { '#e': idChunk, kinds: commentKindsNested, limit: LIMIT }, + { + '#E': idChunk, + kinds: [ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT], + limit: LIMIT + } + ] + const nestedReplies = await queryService.fetchEvents(finalRelayUrls, nestedFilters, { + onevent: (evt: NEvent) => { + if (fetchGeneration !== replyFetchGenRef.current) return + if (shouldHideThreadResponseEvent(evt, mutePubkeySet, hideContentMentioningMutedUsers)) + return + if (!replyMatchesThreadForList(evt, event, rootInfo, isDiscussionRoot)) return + addReplies([evt]) + } + }) + if (fetchGeneration !== replyFetchGenRef.current) return + nestedAccum.push(...nestedReplies) + } + const validNested = nestedAccum.filter( (evt) => !shouldHideThreadResponseEvent(evt, mutePubkeySet, hideContentMentioningMutedUsers) && replyMatchesThreadForList(evt, event, rootInfo, isDiscussionRoot) diff --git a/src/constants.ts b/src/constants.ts index 0e0338ed..f51a8794 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -145,6 +145,13 @@ export const MAX_REQ_RELAY_URLS = MAX_CONCURRENT_RELAY_CONNECTIONS */ export const RELAY_FILTER_MAX_KINDS_PER_OBJECT = 10 +/** + * Maximum NIP-01 filters per REQ (`["REQ", subId, …filters]`). Primal, damus.io, and others return + * NOTICE `bad req: arr too big` when the filter list is long (e.g. replaceable threads with #a + #e + * snapshot + many kind-chunked op-reference filters). + */ +export const RELAY_REQ_MAX_FILTERS_PER_MESSAGE = 10 + /** `SimplePool.ensureRelay` WebSocket handshake timeout (parallel multi-relay + slow TLS). */ export const RELAY_POOL_CONNECTION_TIMEOUT_MS = 20_000 diff --git a/src/services/client-query.service.ts b/src/services/client-query.service.ts index 72afff3d..d3add4f2 100644 --- a/src/services/client-query.service.ts +++ b/src/services/client-query.service.ts @@ -8,6 +8,7 @@ import { MAX_CONCURRENT_RELAY_CONNECTIONS, MAX_CONCURRENT_SUBS_PER_RELAY, RELAY_FILTER_MAX_KINDS_PER_OBJECT, + RELAY_REQ_MAX_FILTERS_PER_MESSAGE, RELAY_POOL_CONNECTION_TIMEOUT_MS, SEARCHABLE_RELAY_URLS } from '@/constants' @@ -293,6 +294,23 @@ export class QueryService { ): Promise { const sanitizedFilters = sanitizeFiltersBeforeReq(filter) if (sanitizedFilters.length === 0) return [] + + const maxFilters = RELAY_REQ_MAX_FILTERS_PER_MESSAGE + if (sanitizedFilters.length > maxFilters) { + const merged: NEvent[] = [] + const seen = new Set() + for (let i = 0; i < sanitizedFilters.length; i += maxFilters) { + const slice = sanitizedFilters.slice(i, i + maxFilters) + const part = await this.query(urls, slice, onevent, options) + for (const e of part) { + if (seen.has(e.id)) continue + seen.add(e.id) + merged.push(e) + } + } + return merged + } + /** One chunk → pass a single Filter (compat); several (e.g. kinds split) → full array for WS + HTTP. */ const effectiveFilter: Filter | Filter[] = sanitizedFilters.length === 1 ? sanitizedFilters[0]! : sanitizedFilters