Skip to content

feat: thread-level muting (mute thread / hellthread mute)#7

Merged
DocNR merged 11 commits into
mainfrom
feat/thread-muting
Jun 13, 2026
Merged

feat: thread-level muting (mute thread / hellthread mute)#7
DocNR merged 11 commits into
mainfrom
feat/thread-muting

Conversation

@DocNR

@DocNR DocNR commented Jun 13, 2026

Copy link
Copy Markdown
Owner

Summary

Adds the ability to mute an entire thread. Pick "Mute thread" from any note's menu and the whole conversation, including every reply, disappears from feeds, notifications, and the unread counts. Stored privately in your NIP-51 kind-10000 mute list, so it syncs across your devices and interops with other clients.

  • One helper, both ends. getThreadRootId(evt) = getRootEventHexId(evt) ?? evt.id (NIP-10 aware) computes a note's conversation root; it's used identically at store-time (mute the clicked note's root) and filter-time (isInMutedThread(evt, set)). O(1) per note, no reply-tree walking, robust with partial threads.
  • Mute by root. Muting from any note (even a deep reply) silences the whole conversation, because every descendant carries the root's id in its NIP-10 root marker.
  • Private only. muteThread writes the ['e', rootId] into the NIP-44-encrypted portion of the mute list; unmuteThread strips from both public and private. Mirrors the existing pubkey-mute pattern (same changing lock, optimistic overlay + rollback).
  • Treated exactly like a muted user, at every tier:
    • Hard-hide in feeds (NoteList, NoteCard), thread reply lists (useFilteredReplies), notifications (NotificationItem), and the unread counts (column badge + favicon/title, via notificationFilter).
    • Collapse + reveal wherever a note still renders through <Note> but isn't list-filtered (quotes/embeds and direct detail views): a <MutedNote reason=\"thread\"> placeholder — "This note is from a thread you muted" + "Temporarily display this note." MutedNote was generalized with a reason prop so user- and thread-mute share one component.
  • Manage it. New "Muted threads" section in Settings → Muted list shows each muted root with an Unmute button.

Ships as v26.12.2 with a "What's new" entry.

Notes for reviewers

  • Mirrors muted-user handling 1:1. Every place that special-cases a muted author now has the parallel muted-thread branch (hard-hide tiers + the <Note> collapse/reveal tier). Feeds + notifications hard-hide; embeds/detail collapse-with-reveal — same as users.
  • muteEventIdSet also honors any pre-existing public e-tag thread mutes already in a user's kind-10000 list (written by other clients). Previously never applied at filter time; now they hide those threads. Strictly more NIP-51-compliant.

Test coverage

22 unit tests on the core predicate + helpers (getThreadRootId, isInMutedThread, getEventIdsFromETags/appendETag/stripETag, notificationFilter thread exclusion). Full suite: 855 passing, npm run build clean.

Manual test plan (needs a signing account)

  • menu on a thread note shows "Mute thread"; clicking it removes the note + its replies from the feed
  • A mention from that thread leaves the Notifications column and the unread badge/count drops
  • A note that quotes the muted thread now shows the embed collapsed to "This note is from a thread you muted" with a "Temporarily display this note" reveal (same as quoting a muted user)
  • Settings → Muted list shows a "Muted threads" section; "Unmute" makes the thread reappear and the row vanishes immediately
  • Reload: thread stays hidden (persisted to kind-10000) and still listed under "Muted threads"
  • Arabic (Settings → Languages → العربية): menu item + section + placeholder render correctly RTL

🤖 Generated with Claude Code

DocNR and others added 11 commits June 13, 2026 17:02
Renumbered from 26.12.0: the concurrent home-tab-persistence work
shipped 26.12.0 and re-bumped to 26.12.1 on main while this branch was
in flight, so thread muting takes the next free patch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Normalizes formatting of the new test files and the MuteListPage
'Muted threads' section. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
NotificationItem already hid muted-thread items at render time, but the
unread badge (per-column header + favicon/title) is driven by a separate
notificationFilter path that did not know about thread mutes. A muted
hellthread that kept tagging you still incremented the badge while the
list showed nothing. Thread the muteEventIdSet through notificationFilter
+ useNotificationFilter so counts and list agree.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The muted-threads memo keyed on [pubkey], copied from the muted-users
list. Users have a MuteButton that masks the stale row, but a thread row
has no such toggle, so an unmuted thread lingered until remount. Key the
memo on [muteEventIdSet] so the row vanishes on unmute.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying jank with  Cloudflare Pages  Cloudflare Pages

Latest commit: 9c698fb
Status: ✅  Deploy successful!
Preview URL: https://3170c011.jank-4ii.pages.dev
Branch Preview URL: https://feat-thread-muting.jank-4ii.pages.dev

View logs

@DocNR DocNR merged commit b2399ae into main Jun 13, 2026
2 checks passed
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.

1 participant