Skip to content

fix: show muted search results as profile cards - #296

Open
dergigi wants to merge 4 commits into
masterfrom
fix/issue-294
Open

fix: show muted search results as profile cards#296
dergigi wants to merge 4 commits into
masterfrom
fix/issue-294

Conversation

@dergigi

@dergigi dergigi commented Jun 17, 2026

Copy link
Copy Markdown
Owner

This keeps is:muted by:... results anchored to the underlying kind 10000 event while rendering the muted p tags as a dedicated profile list, so the raw JSON toggle still works. It also keeps the mute-list loading state in the profile lane and aggregates muted pubkeys across fetched mute-list events for the rendered list. Verified with npm run lint, npm test -- --runInBand, npm run build, and the Vercel preview for is:muted by:fiatjaf.

  • Render kind 10000 mute-list searches as a dedicated profile-list card instead of flattening them into standalone profile results
  • Preserve the original mute-list event so Show raw JSON still reveals the real kind 10000 payload
  • Aggregate unique muted pubkeys across fetched mute-list events and render 21 muted profiles in the preview example

Fixes #294

@vercel

vercel Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ants Ready Ready Preview, Comment Jun 18, 2026 4:52pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

detectSearchType now classifies is:muted and kind:10000 queries as 'profile' using dedicated regex patterns. A new storage layer associates muted pubkeys and resolved profiles with mute-list events via WeakMap. In tryHandleAuthorSearch, two helpers extract unique valid pubkeys from mute-list p tags and resolve them into profile events in bounded batches, storing the expanded data on the representative event. A new MuteListCard component displays muted profiles with author-click handlers, and SearchResultsList conditionally renders it for kind-10000 events.

Changes

Mute-list to profile card expansion

Layer / File(s) Summary
Mute-list result data storage
src/lib/search/muteListResultData.ts
Introduces MuteListResultData type and defensive copy storage/retrieval helpers using WeakMap to associate muted pubkeys and profiles with NDKEvent instances.
Query classification for is:muted / kind:10000
src/lib/search/searchTypeDetection.ts
Adds regex patterns to detect is:muted and kind:10000 tokens and extends the 'profile' detection condition to match these patterns alongside existing p: and by: checks.
Mute-list pubkey extraction and profile resolution
src/lib/search/strategies/authorSearchStrategy.ts
Adds profileEventFromPubkey import; introduces helpers to extract unique valid pubkeys from p tags in newest-first order and resolve them into profiles in bounded batches; routes partial results through mute-list-aware handler; alters return path to store expanded data and emit only the representative event for kind-10000 searches.
Mute-list card component
src/components/MuteListCard.tsx
Introduces MuteListCard component that extracts muted pubkeys and profiles from stored result data, renders a count label and grid of muted-profile rows with author-click handlers or navigation.
Search results integration
src/components/SearchResultsList.tsx
Adds conditional rendering branch that renders MuteListCard for kind-10000 events recognized by getMuteListResultData, passing author-click handler and event-search footer button.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 A mute list was just boring JSON,
Raw tags and pubkeys — the show must go on!
Now kind ten-thousand turns into a face,
Profile cards blooming all over the place.
Hoppy helpers extract, resolve, and then store,
Beautiful profiles displayed evermore! ✨

Possibly related PRs

  • dergigi/ants#155: Modifies detectSearchType to add is:article/is:longform aliases alongside the existing search-type classification logic that this PR extends with is:muted and kind:10000 handling.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main objective: converting mute-list search results from raw JSON to profile cards, which aligns with the core changes across all modified files.
Linked Issues check ✅ Passed The PR implements the core requirement from #294 by extracting pubkeys from mute-list events and displaying them as profile cards. The changes add search type detection for muted queries, expand mute-list results via author search, create a MuteListCard component, and integrate it into search results display.
Out of Scope Changes check ✅ Passed All modifications are directly related to implementing mute-list profile card display: search type detection, author search expansion, new MuteListCard component, result data storage, and integration into SearchResultsList.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-294

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/lib/search/strategies/authorSearchStrategy.ts (1)

43-49: ⚡ Quick win

Cap concurrent profile fetches for large mute lists.

Lines 43-49 fan out one async lookup per pubkey with unbounded Promise.all, which can spike relay/network load on large lists.

Suggested refactor
-  const profiles = await Promise.all(pubkeys.map(async (pubkey) => {
-    try {
-      return await profileEventFromPubkey(pubkey);
-    } catch {
-      return null;
-    }
-  }));
-
-  return profiles.filter((event): event is NDKEvent => event !== null);
+  const BATCH_SIZE = 20;
+  const results: NDKEvent[] = [];
+  for (let i = 0; i < pubkeys.length; i += BATCH_SIZE) {
+    const batch = pubkeys.slice(i, i + BATCH_SIZE);
+    const profiles = await Promise.all(batch.map(async (pubkey) => {
+      try {
+        return await profileEventFromPubkey(pubkey);
+      } catch {
+        return null;
+      }
+    }));
+    results.push(...profiles.filter((event): event is NDKEvent => event !== null));
+  }
+  return results;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/search/strategies/authorSearchStrategy.ts` around lines 43 - 49, The
Promise.all call on the pubkeys.map operation in authorSearchStrategy.ts creates
unbounded concurrent requests that can overwhelm relays on large mute lists. Add
concurrency limiting to cap the number of simultaneous profileEventFromPubkey
calls (e.g., using a concurrency limit utility or batching strategy). Replace
the direct Promise.all with a pattern that processes the pubkeys in controlled
batches or with a semaphore that ensures only a maximum number of concurrent
profile fetches occur at once, then collect all results into the profiles array.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/search/searchTypeDetection.ts`:
- Line 9: The condition on line 9 uses plain substring matching with includes()
for keywords like 'kind:10000' and 'is:muted', which causes false positives when
substrings match longer values (e.g., 'kind:100000' incorrectly matches
'kind:10000'). Replace the plain includes() checks for 'kind:10000' and
'is:muted' with token-boundary matching that only matches when these keywords
are followed by word boundaries (whitespace, end of string, or punctuation)
rather than being part of a longer value. You can use regex with word boundaries
or check that the match is followed by a space or end of string to properly
distinguish between 'kind:10000' and 'kind:100000'.

In `@src/lib/search/strategies/authorSearchStrategy.ts`:
- Around line 183-186: The conditional check in the mute-list expansion logic
for kind 10000 only returns profiles when they exist, but falls through to
return raw events when expansion yields zero results. Modify the logic after
calling expandMuteListResults to explicitly return an empty array when
profiles.length equals zero, ensuring that failed or empty mute-list expansions
do not revert to rendering raw kind 10000 events. This prevents the undesired
fallback behavior and maintains consistent handling of the mute-list expansion
case.

---

Nitpick comments:
In `@src/lib/search/strategies/authorSearchStrategy.ts`:
- Around line 43-49: The Promise.all call on the pubkeys.map operation in
authorSearchStrategy.ts creates unbounded concurrent requests that can overwhelm
relays on large mute lists. Add concurrency limiting to cap the number of
simultaneous profileEventFromPubkey calls (e.g., using a concurrency limit
utility or batching strategy). Replace the direct Promise.all with a pattern
that processes the pubkeys in controlled batches or with a semaphore that
ensures only a maximum number of concurrent profile fetches occur at once, then
collect all results into the profiles array.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8cf9ba19-1fb9-4e1e-aad6-7594203ae23a

📥 Commits

Reviewing files that changed from the base of the PR and between 75f6d9b and b0b3dc1.

📒 Files selected for processing (2)
  • src/lib/search/searchTypeDetection.ts
  • src/lib/search/strategies/authorSearchStrategy.ts

Comment thread src/lib/search/searchTypeDetection.ts Outdated
Comment thread src/lib/search/strategies/authorSearchStrategy.ts Outdated
@dergigi

dergigi commented Jun 18, 2026

Copy link
Copy Markdown
Owner Author

It only shows two profiles? It should show way more I think?

I guess in the example we have in #294 (comment) it should show one profile card per p tag?

@dergigi

dergigi commented Jun 18, 2026

Copy link
Copy Markdown
Owner Author

Hm, not happy with it yet. We'll probably need a profile list element or something. Otherwise we aren't able to switch back to the raw json format.

@dergigi

dergigi commented Jun 18, 2026

Copy link
Copy Markdown
Owner Author

Pushed a follow-up for this.

The result stays anchored to the kind 10000 mute-list event now, but renders the muted p tags as a dedicated profile list inside the card. That means Show raw JSON works again.

Checked the preview for is:muted by:fiatjaf:

  • it renders 21 muted profiles
  • the overflow menu switches back to the real raw kind 10000 JSON

I already re-ran npm run lint, npm test -- --runInBand, and npm run build locally. Waiting on search-smoke now.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/MuteListCard.tsx`:
- Around line 43-50: The click handler logic in handleOpenProfile uses user.npub
directly, but the component computes a fallback npub variable for rendering.
Replace all instances of user.npub with the resolved npub variable in the
onAuthorClick callback and window.location.href navigation to ensure consistency
between what is displayed to the user and where the click navigation actually
directs them. This applies to both the initial click handler block (around lines
43-50) and the secondary handler block (around lines 53-60).

In `@src/lib/search/strategies/authorSearchStrategy.ts`:
- Around line 150-161: The authorSearchStrategy function is passing
chosenRelaySet or broadRelaySet directly to subscribeAndCollect calls without
checking if the filters contain a search property. When filter.search is
present, you must use getNip50SearchRelaySet() instead of the default relay sets
to comply with NIP-50 relay selection requirements. Update all three
subscribeAndCollect calls in the function (the one in the map callback around
line 150, and the two else branches around lines 156 and 159-160) to check
whether the filters have a search property and conditionally pass
getNip50SearchRelaySet() as the relaySet parameter when search is present,
otherwise use the appropriate relay set.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2fbdd6db-ccc5-4005-8866-63df21fb8e2e

📥 Commits

Reviewing files that changed from the base of the PR and between 6d1484b and 39bcac7.

📒 Files selected for processing (4)
  • src/components/MuteListCard.tsx
  • src/components/SearchResultsList.tsx
  • src/lib/search/muteListResultData.ts
  • src/lib/search/strategies/authorSearchStrategy.ts

Comment on lines +43 to +50
if (onAuthorClick) {
onAuthorClick(user.npub);
return;
}

if (typeof window !== 'undefined') {
window.location.href = `/p/${user.npub}`;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use the resolved npub variable for click navigation/callbacks.

The row computes a fallback npub for rendering, but handleOpenProfile still uses user.npub. If fallback logic is hit, click behavior can point to a different/invalid target than what is displayed.

Suggested patch
-  const handleOpenProfile = () => {
+  const handleOpenProfile = () => {
@@
-    if (onAuthorClick) {
-      onAuthorClick(user.npub);
+    if (onAuthorClick) {
+      onAuthorClick(npub);
       return;
     }
@@
-      window.location.href = `/p/${user.npub}`;
+      window.location.href = `/p/${npub}`;
     }
   };

Also applies to: 53-60

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/MuteListCard.tsx` around lines 43 - 50, The click handler
logic in handleOpenProfile uses user.npub directly, but the component computes a
fallback npub variable for rendering. Replace all instances of user.npub with
the resolved npub variable in the onAuthorClick callback and
window.location.href navigation to ensure consistency between what is displayed
to the user and where the click navigation actually directs them. This applies
to both the initial click handler block (around lines 43-50) and the secondary
handler block (around lines 53-60).

Comment on lines +150 to +161
return await subscribeAndCollect(f, { timeoutMs: 8000, relaySet: chosenRelaySet, abortSignal, onPartial: partialResultsHandler });
} catch { return []; }
}));
const seen = new Set<string>();
for (const r of perSeed) {
for (const e of r) { if (!seen.has(e.id)) { seen.add(e.id); res.push(e); } }
}
} else {
res = await subscribeAndCollect(filters, { timeoutMs: 8000, relaySet: chosenRelaySet, abortSignal, onPartial: onPartialResults });
res = await subscribeAndCollect(filters, { timeoutMs: 8000, relaySet: chosenRelaySet, abortSignal, onPartial: partialResultsHandler });
}
} else {
res = await subscribeAndCollect(filters, { timeoutMs: 8000, relaySet: chosenRelaySet, abortSignal, onPartial: onPartialResults });
res = await subscribeAndCollect(filters, { timeoutMs: 8000, relaySet: chosenRelaySet, abortSignal, onPartial: partialResultsHandler });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use getNip50SearchRelaySet() whenever filter.search is present.

These subscriptions can run with search filters but still pass chosenRelaySet/broadRelaySet directly. That violates the relay-selection contract for NIP-50 searches.

Suggested patch
+import { getNip50SearchRelaySet } from '`@/lib/relays/nip50`';
@@
-          return await subscribeAndCollect(f, { timeoutMs: 8000, relaySet: chosenRelaySet, abortSignal, onPartial: partialResultsHandler });
+          const relaySetForSearch = f.search ? await getNip50SearchRelaySet() : chosenRelaySet;
+          return await subscribeAndCollect(f, { timeoutMs: 8000, relaySet: relaySetForSearch, abortSignal, onPartial: partialResultsHandler });
@@
-      res = await subscribeAndCollect(filters, { timeoutMs: 8000, relaySet: chosenRelaySet, abortSignal, onPartial: partialResultsHandler });
+      const relaySetForSearch = filters.search ? await getNip50SearchRelaySet() : chosenRelaySet;
+      res = await subscribeAndCollect(filters, { timeoutMs: 8000, relaySet: relaySetForSearch, abortSignal, onPartial: partialResultsHandler });
@@
-    res = await subscribeAndCollect(filters, { timeoutMs: 10000, relaySet: broadRelaySet, abortSignal, onPartial: partialResultsHandler });
+    const relaySetForSearch = filters.search ? await getNip50SearchRelaySet() : broadRelaySet;
+    res = await subscribeAndCollect(filters, { timeoutMs: 10000, relaySet: relaySetForSearch, abortSignal, onPartial: partialResultsHandler });

As per coding guidelines, "Always pass an explicit NIP-50 relay set (via getNip50SearchRelaySet() in src/lib/relays/nip50.ts) when subscribing with a search filter" and "Never let a search subscription fall through to the default pool or an unfiltered relay set."

Also applies to: 194-194

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/search/strategies/authorSearchStrategy.ts` around lines 150 - 161,
The authorSearchStrategy function is passing chosenRelaySet or broadRelaySet
directly to subscribeAndCollect calls without checking if the filters contain a
search property. When filter.search is present, you must use
getNip50SearchRelaySet() instead of the default relay sets to comply with NIP-50
relay selection requirements. Update all three subscribeAndCollect calls in the
function (the one in the map callback around line 150, and the two else branches
around lines 156 and 159-160) to check whether the filters have a search
property and conditionally pass getNip50SearchRelaySet() as the relaySet
parameter when search is present, otherwise use the appropriate relay set.

Source: Coding guidelines

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

is:muted should show results as profile cards

1 participant