From 5ff9bbd5c2eec1bf487f577b18a517838b96b171 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20G=C3=B3mez?= Date: Tue, 24 Mar 2026 11:49:46 +0100 Subject: [PATCH] fix: use aggregator relays as NIP-65 fallback and show accurate per-relay counts - Use AGGREGATOR_RELAYS as fallback for follows without kind:10002, not the user's personal relays. Personal inbox/write relays were being assigned as outbox for hundreds of unknown follows, inflating counts and sending unnecessary queries to niche relays. - Per-relay REQ badges now show assigned count (from reasoning) as the primary number, with unassigned users shown dimmed as +N. Tooltips show the full breakdown. - Switch to useStableRelayFilterMap for structural comparison. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/ReqViewer.tsx | 74 +++++++++++++++++++++++++----------- 1 file changed, 52 insertions(+), 22 deletions(-) diff --git a/src/components/ReqViewer.tsx b/src/components/ReqViewer.tsx index d683d72..7c3a827 100644 --- a/src/components/ReqViewer.tsx +++ b/src/components/ReqViewer.tsx @@ -88,7 +88,7 @@ import { } from "@/lib/req-state-machine"; import { resolveFilterAliases, getTagValues } from "@/lib/nostr-utils"; import { chunkFiltersByRelay } from "@/lib/relay-filter-chunking"; -import { useStableValue } from "@/hooks/useStable"; +import { useStableRelayFilterMap } from "@/hooks/useStable"; import { useNostrEvent } from "@/hooks/useNostrEvent"; import { MemoizedCompactEventRow } from "./nostr/CompactEventRow"; @@ -694,17 +694,28 @@ function QueryDropdown({
{Object.entries(relayFilterMap).map( ([relayUrl, relayFilters]) => { - const authorCount = relayFilters.reduce( + const reasoning = relayReasoning?.find( + (r) => r.relay === relayUrl, + ); + const isFallback = !!reasoning?.isFallback; + + // Use reasoning counts (assigned users) not raw filter counts + // (which include unassigned users piggybacking on every relay) + const assignedWriters = reasoning?.writers?.length || 0; + const assignedReaders = reasoning?.readers?.length || 0; + + // Total in chunked filter for unassigned calculation + const totalAuthors = relayFilters.reduce( (sum, f) => sum + (f.authors?.length || 0), 0, ); - const pTagCount = relayFilters.reduce( + const totalPTags = relayFilters.reduce( (sum, f) => sum + (f["#p"]?.length || 0), 0, ); - const isFallback = !!relayReasoning?.find( - (r) => r.relay === relayUrl, - )?.isFallback; + const unassignedAuthors = totalAuthors - assignedWriters; + const unassignedPTags = totalPTags - assignedReaders; + const relayJson = JSON.stringify( relayFilters.length === 1 ? relayFilters[0] : relayFilters, null, @@ -718,22 +729,42 @@ function QueryDropdown({
- {authorCount > 0 && ( + {(assignedWriters > 0 || + (isFallback && totalAuthors > 0)) && ( 0 ? ` + ${unassignedAuthors} unassigned` : ""} (${totalAuthors} total in REQ)` + } > - {authorCount} + {isFallback ? totalAuthors : assignedWriters} + {!isFallback && unassignedAuthors > 0 && ( + + +{unassignedAuthors} + + )} )} - {pTagCount > 0 && ( + {(assignedReaders > 0 || + (isFallback && totalPTags > 0)) && ( 0 ? ` + ${unassignedPTags} unassigned` : ""} (${totalPTags} total in REQ)` + } > - {pTagCount} + {isFallback ? totalPTags : assignedReaders} + {!isFallback && unassignedPTags > 0 && ( + + +{unassignedPTags} + + )} )} {isFallback && ( @@ -850,22 +881,21 @@ export default function ReqViewer({ // We just display the NIP-05 identifiers for user reference // NIP-65 outbox relay selection - // Memoize fallbackRelays to prevent re-creation on every render - const fallbackRelays = useMemo( - () => - state.activeAccount?.relays?.filter((r) => r.read).map((r) => r.url) || - AGGREGATOR_RELAYS, - [state.activeAccount?.relays], - ); + // Fallback relays for follows without kind:10002 relay lists. + // Use AGGREGATOR_RELAYS (popular general relays), NOT the user's personal relays. + // The user's relays (both read and write) are specific to their network — + // assigning them as outbox for hundreds of unknown follows inflates counts + // and sends unnecessary queries to small/niche relays. + const fallbackRelays = AGGREGATOR_RELAYS; - // Memoize outbox options to prevent object re-creation + // Stable outbox options (fallbackRelays is a module constant) const outboxOptions = useMemo( () => ({ fallbackRelays, timeout: 1000, maxRelays: 42, }), - [fallbackRelays], + [], ); // Select optimal relays based on authors (write relays) and #p tags (read relays) @@ -916,7 +946,7 @@ export default function ReqViewer({ return chunkFiltersByRelay(resolvedFilter, reasoning); }, [relays, reasoning, resolvedFilter]); - const stableRelayFilterMap = useStableValue(relayFilterMap); + const stableRelayFilterMap = useStableRelayFilterMap(relayFilterMap); const { events,