Skip to content

feat(desktop): thread-aware notifications with mutable follow/mute controls#761

Open
wpfleger96 wants to merge 7 commits into
mainfrom
worktree-wpfleger+thread-notify
Open

feat(desktop): thread-aware notifications with mutable follow/mute controls#761
wpfleger96 wants to merge 7 commits into
mainfrom
worktree-wpfleger+thread-notify

Conversation

@wpfleger96
Copy link
Copy Markdown
Collaborator

@wpfleger96 wpfleger96 commented May 27, 2026

Sprout was notifying on every channel message, including thread replies from threads the user has no involvement in. This PR brings notification behavior in line with Slack's model: thread replies only notify when the user has a stake in the thread, and users have full control over opting in or out.

Notification filtering (shouldNotifyForEvent)

Client-side predicate gating notifications before badge/bounce fire. A thread reply triggers a notification only when:

  • The reply is a broadcast reply (visible in main timeline)
  • The reply p-tags the current user (explicit @-mention, overrides mute)
  • The thread is NOT in the user's mutedRootIds denylist, AND at least one of:
    • The user replied to the thread (participatedRootIds)
    • The user authored the thread root (authoredRootIds)
    • The user explicitly followed the thread (followedRootIds)

Top-level messages and DM notifications are completely unaffected.

Mutable follow/mute controls

Users can follow or unfollow ANY thread, including ones they authored or participated in. The UI is a simple two-state toggle:

  • Notified (followed, participated, or authored and not muted) -- "Unfollow thread" action, which removes from followedRootIds and adds to mutedRootIds
  • Not notified (muted, or no stake in the thread) -- "Follow thread" action, which adds to followedRootIds and removes from mutedRootIds

mutedRootIds is a localStorage-persisted denylist (sprout-thread-muted.v1:{pubkey}, 1000-entry cap) that overrides participation, follow, and authorship signals. Explicit @-mentions (p-tag) override the mute.

Thread activity feed

Thread reply notifications populate threadActivityItems, injected as synthetic FeedItems with category: "activity" into the Home feed. The "All" tab includes activity items alongside mentions and needs-action items.

Thread activity is persisted to localStorage (sprout-thread-activity.v1:{pubkey}, 100-item cap) so it survives restarts. Both live events and catch-up REQs populate it. Thread replies also fire onChannelMessage for channel unread badges and dock bounce.

Other details

  • useThreadFollows -- localStorage-backed follow set per pubkey, 500-entry LRU cap, cross-tab storage event sync
  • authoredRootIds -- tracks top-level messages authored by the user, populated from live events and catch-up REQs
  • Thread participation tracked via two-pass catch-up REQ (collect self-authored replies first, then filter external events) and live onSelfChannelMessage
  • Follow/Unfollow actions available on both thread panel header and main timeline thread-root messages via MoreActionsMenu
  • 23-case unit test suite for shouldNotifyForEvent covering all precedence combinations including mute overrides

@wpfleger96 wpfleger96 requested a review from a team as a code owner May 27, 2026 19:55
@wpfleger96 wpfleger96 force-pushed the worktree-wpfleger+thread-notify branch 2 times, most recently from b033b6e to b203463 Compare May 27, 2026 20:48
@wpfleger96
Copy link
Copy Markdown
Collaborator Author

new "Follow thread" menu item:
image

Menu changes once a thread has been followed:
image

@wpfleger96 wpfleger96 changed the title feat(desktop): thread-aware notification filtering + follow thread action feat(desktop): thread-aware notification filtering + follow thread action + activity feed May 27, 2026
…tion

All channel messages — including thread replies from threads the user has
no involvement in — triggered equal badge/bounce/toast noise. Adds a
client-side notification filter so only top-level messages, broadcast
replies, and thread replies in participated/followed threads fire
notifications. Participation is detected from the user's own replies via
the existing catch-up REQ and a new onSelfChannelMessage live path.

Also adds a "Follow thread" / "Unfollow thread" action to the message
action bar in the thread panel, backed by a localStorage-persisted per-
pubkey follow set (500-entry LRU cap, v1 key with cross-device sync
deferred to a future NIP-RS extension).
Thread authors weren't auto-notified when someone replied to their post
because top-level messages have no NIP-10 root tag. Added p-tag check to
shouldNotifyForEvent so replies that include the author's pubkey in a p
tag (Nostr convention) trigger notifications. Also persisted
participatedRootIds to localStorage so participation survives restarts,
fixed writeToStorage silently swallowing quota errors, extracted shared
isBroadcastReply helper, deduplicated EMPTY_SET, added follow-thread
action to main timeline thread roots, and added cross-tab storage sync.
…ctivity feed

Thread authors weren't notified about replies because buildReplyTags
adds the replier's pubkey, not the root author's. Rather than fix
p-tags, track authored root IDs client-side (same pattern as
participatedRootIds) and check them in shouldNotifyForEvent.

The "Follow thread" button only checked explicit follows, showing
"Follow thread" even when the user was already notified via authorship
or participation. Exposed a combined isNotifiedForThread predicate
and show a disabled "Following" indicator for auto-notified threads.

Thread reply notifications only manifested as channel unread badges
with no way to identify which thread triggered them. Split the
notification path so thread replies route to a new Home activity feed
instead of channel badges, making them visible in the Activity tab.
…b filter

Thread replies were routed exclusively to the activity feed, losing
channel unread badges and dock bounce. The if/else in
useLiveChannelUpdates now fires onChannelMessage unconditionally,
then additionally fires onThreadReplyNotification for thread replies.

Users could not opt out of notifications for threads they authored or
participated in. A mutedRootIds denylist (localStorage-persisted,
identity-scoped) now lets users mute any thread. The UI collapses to
a two-state Follow/Unfollow toggle. Mute precedence: p-tag mentions
override mute, mute overrides participation/follow/authorship.

The "All" tab in Home excluded pure-activity items; it now includes
everything.
@wpfleger96 wpfleger96 force-pushed the worktree-wpfleger+thread-notify branch from 25b7a98 to fc8452f Compare May 27, 2026 23:07
@wpfleger96 wpfleger96 changed the title feat(desktop): thread-aware notification filtering + follow thread action + activity feed feat(desktop): thread-aware notifications with mutable follow/mute controls May 27, 2026
wpfleger96 and others added 3 commits May 27, 2026 19:35
The badge hook's currentFeedItems included feed.feed.activity, which
inflated the seen-set and badge count. Activity items from the relay
API are always empty in production (Rust returns Vec::new()), but the
E2E mock feed seeds them, breaking two tests. Thread activity items
surface via HomeScreen's augmented feed for display only — they should
not participate in the badge/seen-set mechanism.
…et seed

collectHomeAlertItems was including feed.feed.activity, causing the
first-load seed in useFeedDesktopNotifications to write 4 IDs to
localStorage instead of the expected 2 (mentions + needsAction only).
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