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
This commit is contained in:
Claude
2026-01-13 09:03:14 +00:00
parent 0f948b83b0
commit 2f202286db
3 changed files with 55 additions and 4 deletions

View File

@@ -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({
</div>
)}
{/* Interest List Warning Banner */}
{interestListStatus === "not-found" && (
<div className="border-b border-border px-4 py-2 bg-warning/10 flex items-center gap-2">
<Hash className="size-4 text-warning" />
<span className="text-xs text-warning">
No interest list found (kind 10015).{" "}
<code className="bg-muted px-1 py-0.5 rounded">$hashtags</code>{" "}
ignored.
</span>
</div>
)}
{interestListStatus === "empty" && (
<div className="border-b border-border px-4 py-2 bg-warning/10 flex items-center gap-2">
<Hash className="size-4 text-warning" />
<span className="text-xs text-warning">
Interest list has no hashtags.{" "}
<code className="bg-muted px-1 py-0.5 rounded">$hashtags</code>{" "}
ignored.
</span>
</div>
)}
{/* Account Required Error */}
{(needsAccount || needsInterestList) && !accountPubkey && (
<div className="flex flex-col items-center justify-center h-full gap-4 p-8 text-center">

View File

@@ -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();
});
});

View File

@@ -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;