fix(workspaces): exclude muted channels and unfollowed threads from rail unread badge#1738
Merged
Conversation
Muted channels with new external messages were lighting the workspace rail's unread dot because fetchObservedChannels filtered archived channels and hidden DMs but not muted channels. Fetch the channel-mutes blob (kind 30078, d=channel-mutes) alongside read-state using Promise.all, decrypt via nip44DecryptFromSelf, derive the muted id set with mutedChannelIdsFromStore, and skip muted channels in the unread/mention loop. Decryption failure or absent blob falls back to empty mutes set — no regression for users with no mutes. Adds injectable decryptMutes parameter (mirrors existing decryptReadState pattern) so the unit test can stub the crypto call without a real key. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
wpfleger96
added a commit
that referenced
this pull request
Jul 10, 2026
…message The rail unread dot was firing for threaded replies in threads the user was not following, participating in, or mentioned in — the same events the in-workspace sidebar dot correctly ignores. Port shouldNotifyForEvent (the exact gate the sidebar catch-up uses in useLiveChannelUpdates) into fetchWorkspaceUnread. Before the per-channel loop, read the four by-pubkey localStorage sets — participated, followed, authored, muted-roots — and pass them as the gate options. The channel- level mute set computed by #1738 is also forwarded for redundant safety. The follows set uses ThreadFollowEntry[] format (not the plain string[] that makeRootIdStore reads) so it gets a thin dedicated reader. The four sets are injectable via readThreadRelationships? so unit tests can supply sets directly without touching window.localStorage. Effect: threaded replies in untracked threads no longer light the rail dot. Broadcast replies, #p-mentions, top-level posts, and replies in followed/participated/authored threads still do — identical to the in-workspace badge. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…message The rail unread dot was firing for threaded replies in threads the user was not following, participating in, or mentioned in — the same events the in-workspace sidebar dot correctly ignores. Port shouldNotifyForEvent (the exact gate the sidebar catch-up uses in useLiveChannelUpdates) into fetchWorkspaceUnread. Before the per-channel loop, read the four by-pubkey localStorage sets — participated, followed, authored, muted-roots — and pass them as the gate options. The channel- level mute set computed by #1738 is also forwarded for redundant safety. The follows set uses ThreadFollowEntry[] format (not the plain string[] that makeRootIdStore reads) so it gets a thin dedicated reader. The four sets are injectable via readThreadRelationships? so unit tests can supply sets directly without touching window.localStorage. Effect: threaded replies in untracked threads no longer light the rail dot. Broadcast replies, #p-mentions, top-level posts, and replies in followed/participated/authored threads still do — identical to the in-workspace badge. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
8381699 to
9eb3034
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Two classes of events were incorrectly lighting the workspace rail's unread dot:
hasUnreadbecausefetchObservedChannelsfiltered archived channels and hidden DMs but not muted channels.When the user switched into the relay, both badges disappeared — the two code paths disagreed on what counts as unread.
Root cause
fetchWorkspaceUnreadinworkspaceUnreadObserver.ts:30078,d=channel-mutes)hasUnreadon any external unread event viaisUnreadExternalEvent, with no thread-relevance gateThe in-workspace sidebar uses
shouldNotifyForEvent(a pure function inshouldNotify.ts) to gate events through the same thread-relationship sets. The rail observer did not.Fix
Commit 1 — muted channels:
Fetch the
channel-mutesblob (kind30078,d=channel-mutes, nip44-self-encrypted) alongside read-state in a singlePromise.all, decrypt vianip44DecryptFromSelf, derive the muted id set withmutedChannelIdsFromStore, andcontinuepast muted channels in the loop.Commit 2 — thread-relevance gate:
Before the per-channel loop, read the four by-pubkey localStorage sets — participated (
buzz-thread-participation.v1), followed (buzz-thread-follows.v1), authored (buzz-thread-authored.v1), muted-roots (buzz-thread-muted.v1) — and gate each candidate unread event throughshouldNotifyForEventin addition to the existingisUnreadExternalEventcheck. The follows set usesThreadFollowEntry[]format so it gets a thin dedicated reader.Both injectables follow the existing
decryptReadState/decryptMutespattern for unit-testability.Tests
14 tests total (9 pre-existing + 5 new for thread-relevance gate):
{hasUnread: false, mentionCount: 0}hasUnread: falseparticipatedRootIds→hasUnread: true#p-mention reply in untracked root →hasUnread: true(mention overrides gate)hasUnread: true(no thread gate for top-level)mutedRootIds→hasUnread: false(mute wins even if participated)tsc --noEmitclean,pnpm buildclean.