From 2f202286db7f524c61367a2e9e7e2f8f175945fb Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Jan 2026 09:03:14 +0000 Subject: [PATCH] fix: handle missing/empty interest list gracefully for $hashtags - Remove #t from filter when $hashtags resolves to empty array (prevents unusable queries) - Preserve other hashtags when $hashtags is empty - Add UI warning banners for: - Interest list not found (kind 10015) - Interest list exists but has no hashtags - Add interestListStatus computed state for UI feedback - Add tests for empty $hashtags resolution scenarios --- src/components/ReqViewer.tsx | 32 ++++++++++++++++++++++++++++++++ src/lib/nostr-utils.test.ts | 18 +++++++++++++++--- src/lib/nostr-utils.ts | 9 ++++++++- 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/src/components/ReqViewer.tsx b/src/components/ReqViewer.tsx index 8c8e903..efa46d3 100644 --- a/src/components/ReqViewer.tsx +++ b/src/components/ReqViewer.tsx @@ -704,6 +704,16 @@ export default function ReqViewer({ [interestListEvent], ); + // Compute interest list status for UI feedback + const interestListStatus = useMemo(() => { + if (!needsInterestList) return null; + if (!accountPubkey) return null; // Account required error handles this + if (interestListEvent === undefined) return "loading"; + if (interestListEvent === null) return "not-found"; + if (hashtags.length === 0) return "empty"; + return "ok"; + }, [needsInterestList, accountPubkey, interestListEvent, hashtags.length]); + // Resolve $me, $contacts, and $hashtags aliases (memoized to prevent unnecessary object creation) const resolvedFilter = useMemo( () => @@ -1285,6 +1295,28 @@ export default function ReqViewer({ )} + {/* Interest List Warning Banner */} + {interestListStatus === "not-found" && ( +
+ + + No interest list found (kind 10015).{" "} + $hashtags{" "} + ignored. + +
+ )} + {interestListStatus === "empty" && ( +
+ + + Interest list has no hashtags.{" "} + $hashtags{" "} + ignored. + +
+ )} + {/* Account Required Error */} {(needsAccount || needsInterestList) && !accountPubkey && (
diff --git a/src/lib/nostr-utils.test.ts b/src/lib/nostr-utils.test.ts index 6983ed1..ac3e933 100644 --- a/src/lib/nostr-utils.test.ts +++ b/src/lib/nostr-utils.test.ts @@ -428,13 +428,24 @@ describe("resolveFilterAliases", () => { expect(result["#t"]).not.toContain("$hashtags"); }); - it("should handle $hashtags with empty hashtag list", () => { + it("should remove #t from filter when $hashtags resolves to empty", () => { const filter: NostrFilter = { "#t": ["$hashtags"] }; const result = resolveFilterAliases(filter, undefined, [], { hashtags: [], }); - expect(result["#t"]).toEqual([]); + // Empty #t should be removed from filter entirely + expect(result["#t"]).toBeUndefined(); + }); + + it("should preserve other hashtags when $hashtags resolves to empty", () => { + const filter: NostrFilter = { "#t": ["$hashtags", "nostr", "bitcoin"] }; + const result = resolveFilterAliases(filter, undefined, [], { + hashtags: [], + }); + + // Other hashtags should be preserved + expect(result["#t"]).toEqual(["nostr", "bitcoin"]); }); it("should preserve other hashtags when resolving $hashtags", () => { @@ -520,7 +531,8 @@ describe("resolveFilterAliases", () => { const result = resolveFilterAliases(filter, accountPubkey, []); expect(result.authors).toEqual([accountPubkey]); - expect(result["#t"]).toEqual([]); // Empty when no hashtags provided + // Empty #t should be removed from filter entirely + expect(result["#t"]).toBeUndefined(); }); }); diff --git a/src/lib/nostr-utils.ts b/src/lib/nostr-utils.ts index 163e191..d7df37b 100644 --- a/src/lib/nostr-utils.ts +++ b/src/lib/nostr-utils.ts @@ -236,7 +236,14 @@ export function resolveFilterAliases( } // Deduplicate - resolved["#t"] = Array.from(new Set(resolvedTTags)); + const deduped = Array.from(new Set(resolvedTTags)); + + // If result is empty, remove #t from filter entirely to avoid unusable query + if (deduped.length === 0) { + delete resolved["#t"]; + } else { + resolved["#t"] = deduped; + } } return resolved;