feat(desktop): dock bounce, mark-as-read toggle, and bulk mark-all-read#753
Merged
Merged
Conversation
d5d7f6a to
711c5af
Compare
…l-read Sprout's dock icon gave no visual cue when notifications arrived. Steal three UX patterns from Slack/Discord: dock bounce on new messages, a bidirectional "Mark as read"/"Mark unread" context menu toggle in the sidebar, and a "Mark ALL as read" bulk action with Shift+Esc. Dock bounce uses Tauri's requestUserAttention(Informational) gated on document.hidden so it only fires when the app is backgrounded. Escape marks the current conversation read; Shift+Escape marks all channels read. Both respect defaultPrevented to avoid conflicts with the find bar, message editor, and settings overlay.
Stabilize callback identities to prevent unnecessary re-renders: useEffectEvent for onChannelMessage, ref-based unreadChannelIds for markAllChannelsRead. Replace Date.now() fallback with null guard to avoid marking channels read with a future timestamp. Pass primitives to useMarkAsReadShortcuts instead of the full Channel object, switch from useLayoutEffect to useEffect, tighten the SidebarSection context menu guard to prevent empty menus, and add a title tooltip to the mark-all-read button.
…nnels to badge document.hidden only flips when the window is minimized or hidden — not when another app has focus. document.hasFocus() correctly returns false whenever Sprout is not the active app, so the dock bounce now fires in the common case (user switches to Slack/browser). The dock badge was driven solely by homeBadgeCount (Home Feed mentions/needsAction), which stays 0 for regular channel messages. Sum unreadChannelIds.size so the badge reflects sidebar unread state.
The dock badge now sums unreadChannelIds.size + homeBadgeCount. When a mention arrives in a non-active channel, both signals increment: the channel becomes unread (1) and the home feed gains a mention (1), yielding a badge of 2 instead of the previous 1.
useUnreadChannels received all channels from the relay, including non-member channels. Since non-member channels have no NIP-RS read markers (readAt === null), any relay activity made them permanently "unread" — producing phantom badge counts and dock bounces for activity invisible in the sidebar. Pass sidebarChannels (member + non-archived) instead of all channels so unreadChannelIds, the live subscription, and the catch-up query are scoped to channels the user actually sees. Also guard onChannelMessage with channelId !== activeChannelId to suppress bounces for the channel the user is currently viewing.
409bf8f to
228cb7f
Compare
The dock badge now sums unreadChannelIds.size + homeBadgeCount. Seeded test channels start with unread messages (no NIP-RS read markers), so the initial badge count is non-zero. Capture the baseline after navigating to general and offset all subsequent assertions so the test stays focused on home-badge toggle behaviour.
…el unread The mock mention is injected via __SPROUT_E2E_PUSH_MOCK_FEED_ITEM__ which only adds to homeBadgeCount, not unreadChannelIds (no live relay event is created). The badge increment from the mention is +1, not +2.
Collaborator
|
@codex please review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 86739fa447
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
handleChannelNotification was unconditionally calling requestDockBounce, bypassing the user's notification preference. The DM path guards on notificationSettings.settings.desktopEnabled — match that behaviour for channel messages so disabling Desktop alerts also suppresses bounces.
wesbillman
approved these changes
May 27, 2026
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.
Summary
Adds three complementary notification UX improvements so unread state is visually obvious and easy to dismiss.
The dock badge was previously driven only by Home Feed items and the unread tracking subscribed to every channel on the relay — including non-member channels that have no NIP-RS read markers and are invisible in the sidebar. This scopes all unread tracking to
sidebarChannels(member + non-archived), wiresunreadChannelIds.sizeinto the badge count, and adds dock bounce, bidirectional context menu toggle, bulk mark-all-read, and keyboard shortcuts.requestUserAttention(Informational)fires when!document.hasFocus()for messages in non-active member channels, using a stableuseEffectEventcallback. UseshasFocus()instead ofdocument.hiddenbecause the Page Visibility API only flips when the window is minimized/hidden, not when another app has keyboard focus.onChannelMessageis guarded withchannelId !== activeChannelId(matching the existing DM handler pattern) to suppress bounces for the channel the user is currently viewingsetDesktopAppBadgeCount(unreadChannelIds.size + homeBadgeCount)— sums sidebar unread channels with Home Feed notifications.useUnreadChannelsnow receivessidebarChannelsinstead of all channels, so the badge and live subscription are scoped to member + non-archived channels only — no phantom unreads from non-member relay activityCheckCircle2); right-click a read channel → "Mark unread" (CircleDot). Per-channel guard prevents empty context menus when only one handler is providedCheckCheckbutton in sidebar section headers (visible on hover when unreads exist) +markAllChannelsReadcallback that iteratesunreadChannelIdsvia a ref for stable identity, skipping channels with no resolvable timestamp to avoid future-dated read markersEscmarks current channel as read,Shift+Escmarks all.useEffectwithwindow.addEventListener("keydown")+event.defaultPreventedguard so existing Escape handlers (find bar, message editor, settings overlay) always win. Accepts primitiveactiveChannelId/activeChannelLastMessageAtinstead of the fullChannelobject to avoid listener churn on background query refetchescore:window:allow-request-user-attentioncapability andmark-current-read/mark-all-readentries to the keyboard shortcuts registryScreenshots
Base/default icon (no unread messages):

4 unread channels -> 4 unreads in dock icon badge:


System dock icon bounces and adds unread badge as notifications arrive:
