Skip to content

feat(desktop): dock bounce, mark-as-read toggle, and bulk mark-all-read#753

Merged
wpfleger96 merged 9 commits into
mainfrom
worktree-wpfleger+notification-ux-improvements
May 27, 2026
Merged

feat(desktop): dock bounce, mark-as-read toggle, and bulk mark-all-read#753
wpfleger96 merged 9 commits into
mainfrom
worktree-wpfleger+notification-ux-improvements

Conversation

@wpfleger96
Copy link
Copy Markdown
Collaborator

@wpfleger96 wpfleger96 commented May 26, 2026

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), wires unreadChannelIds.size into the badge count, and adds dock bounce, bidirectional context menu toggle, bulk mark-all-read, and keyboard shortcuts.

  • Dock bounce: requestUserAttention(Informational) fires when !document.hasFocus() for messages in non-active member channels, using a stable useEffectEvent callback. Uses hasFocus() instead of document.hidden because the Page Visibility API only flips when the window is minimized/hidden, not when another app has keyboard focus. onChannelMessage is guarded with channelId !== activeChannelId (matching the existing DM handler pattern) to suppress bounces for the channel the user is currently viewing
  • Dock badge: setDesktopAppBadgeCount(unreadChannelIds.size + homeBadgeCount) — sums sidebar unread channels with Home Feed notifications. useUnreadChannels now receives sidebarChannels instead of all channels, so the badge and live subscription are scoped to member + non-archived channels only — no phantom unreads from non-member relay activity
  • Context menu toggle: right-click an unread channel → "Mark as read" (CheckCircle2); right-click a read channel → "Mark unread" (CircleDot). Per-channel guard prevents empty context menus when only one handler is provided
  • Mark all as read: CheckCheck button in sidebar section headers (visible on hover when unreads exist) + markAllChannelsRead callback that iterates unreadChannelIds via a ref for stable identity, skipping channels with no resolvable timestamp to avoid future-dated read markers
  • Keyboard shortcuts: Esc marks current channel as read, Shift+Esc marks all. useEffect with window.addEventListener("keydown") + event.defaultPrevented guard so existing Escape handlers (find bar, message editor, settings overlay) always win. Accepts primitive activeChannelId/activeChannelLastMessageAt instead of the full Channel object to avoid listener churn on background query refetches
  • Adds core:window:allow-request-user-attention capability and mark-current-read/mark-all-read entries to the keyboard shortcuts registry

Screenshots

Base/default icon (no unread messages):
image

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

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

@wpfleger96 wpfleger96 force-pushed the worktree-wpfleger+notification-ux-improvements branch 2 times, most recently from d5d7f6a to 711c5af Compare May 26, 2026 22:16
…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.
@wpfleger96 wpfleger96 force-pushed the worktree-wpfleger+notification-ux-improvements branch from 409bf8f to 228cb7f Compare May 26, 2026 23:24
@wpfleger96 wpfleger96 marked this pull request as ready for review May 26, 2026 23:41
@wpfleger96 wpfleger96 requested a review from a team as a code owner May 26, 2026 23:41
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.
@wesbillman
Copy link
Copy Markdown
Collaborator

@codex please review

@wpfleger96 wpfleger96 enabled auto-merge (squash) May 27, 2026 00:07
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 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".

Comment thread desktop/src/app/AppShell.tsx
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.
@wpfleger96 wpfleger96 closed this May 27, 2026
auto-merge was automatically disabled May 27, 2026 00:14

Pull request was closed

@wpfleger96 wpfleger96 reopened this May 27, 2026
@wpfleger96 wpfleger96 enabled auto-merge (squash) May 27, 2026 00:15
@wpfleger96 wpfleger96 merged commit 7df4681 into main May 27, 2026
15 checks passed
@wpfleger96 wpfleger96 deleted the worktree-wpfleger+notification-ux-improvements branch May 27, 2026 00:38
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.

2 participants