Skip to content

feat(desktop): keyboard shortcut for next/previous channel in sidebar - #3377

Open
enzowilliam wants to merge 1 commit into
block:mainfrom
enzowilliam:feat/desktop-channel-nav-shortcut
Open

feat(desktop): keyboard shortcut for next/previous channel in sidebar#3377
enzowilliam wants to merge 1 commit into
block:mainfrom
enzowilliam:feat/desktop-channel-nav-shortcut

Conversation

@enzowilliam

Copy link
Copy Markdown

Problem

There is no keyboard path to move between channels — every switch requires the mouse or a ⌘K round-trip. Closes #3078.

Duplicate check: searched open PRs and issues — none found beyond issue #3078 itself. The issue author sketched this exact design in the issue body; this PR follows that spec as written. If they have work in flight, happy to defer or hand this over.

What this adds

⌥↓ / ⌥↑ (macOS) and Ctrl+Alt+↓ / Ctrl+Alt+↑ (Windows/Linux) move the active channel selection down/up the sidebar list. Windows/Linux includes Ctrl because plain Alt+arrows are already back/forward (useBackForwardControls.ts).

Per the issue spec:

  • Order matches the sidebar exactly: starred → custom sections in section order → unassigned, each honoring its own sort preference.
  • Scope is the active community's stream channels; forums and DMs keep the combo's default behavior.
  • Muted channels are skipped.
  • Stops at both ends — no wraparound.
  • No-op on the home feed or when the selection isn't in the list.
  • Registered under Navigation in keyboard-shortcuts.ts, so it shows up in Settings → Keyboard Shortcuts automatically.

Implementation

  • features/sidebar/lib/sidebarChannelOrder.ts — the sidebar's displayed-order flattening, extracted from AppSidebar.tsx into a shared helper (buildSidebarChannelGroups / flattenSidebarChannelGroups / adjacentSidebarChannelId) so the shortcut and the sidebar can't drift. AppSidebar now renders from it; rendering is unchanged.
  • app/useChannelNavigationShortcuts.ts — new hook mirroring useMarkAsReadShortcuts, wired in AppShell. Section membership and per-group sort prefs are read from their relay-scoped localStorage stores at keypress time — the same stores the sidebar hooks keep in sync (including remote NIP-78 updates) — avoiding a duplicate relay subscription.
  • app/useGlobalActionShortcuts.ts — the pre-existing inline ⌘K/⇧⌘K/⇧⌘N/⇧⌘O/⇧⌘A keydown effect moved verbatim out of AppShell. Not drive-by churn: the addition pushed AppShell.tsx over the enforced 1000-line ceiling (check:file-sizes says split, never bump). AppShell is now 951 lines.

Tests

  • 11 unit tests for the ordering/adjacency helper (sidebarChannelOrder.test.mjs): section order, per-group sort, orphaned assignments, muted skipping, boundary stops, empty/unselected no-ops.
  • New e2e case in navigation.spec.ts: steps through sidebar channels without wrapping, platform-aware combo. Ran with the smoke project: 13 passed, 1 skipped (pre-existing test.fixme).
  • Full desktop unit suite: 3732/3732 pass. pnpm typecheck, biome, pnpm check (file sizes, px-text, pubkey-truncation): pass.
  • just ci passes locally.

Manual test

  1. Join a community with a few channels; star one, mute another, put one in a custom section.
  2. Select a channel, press ⌥↓ / ⌥↑ (or Ctrl+Alt+↓/↑): selection walks the sidebar order, skips the muted channel, stops at the ends.
  3. Open a DM or the home feed: the combo does nothing / keeps its default behavior.
  4. Settings → Keyboard Shortcuts shows both entries under Navigation.

Deferred (per the issue)

  • Collapse-awareness: navigating into a collapsed group switches the channel but the group stays shut (collapse state is local to AppSidebar; lifting it is explicitly deferred in the issue).
  • ⌥⇧↓ / ⌥⇧↑ for next/previous unread.

Screenshots follow in a comment (via scripts/post-screenshots.sh).

🤖 Generated with Claude Code

Adds Option+Down/Option+Up (macOS) and Ctrl+Alt+Down/Ctrl+Alt+Up
(Windows/Linux) to move the active channel selection down/up the
sidebar's stream-channel list. Windows/Linux includes Ctrl because
plain Alt+arrows are bound to back/forward navigation.

- Extracts the sidebar's displayed-order flattening (starred -> custom
  sections in order -> unassigned, each honoring its own sort
  preference) into a shared sidebarChannelOrder helper used by both
  AppSidebar and the new useChannelNavigationShortcuts hook, so the
  two cannot drift.
- Muted channels are skipped; the selection stops at both ends (no
  wraparound); no-op on the home feed, forums, and DMs.
- Registers both shortcuts in the Navigation category so they appear
  in Settings -> Keyboard Shortcuts.
- Extracts the inline global action shortcut effect from AppShell into
  useGlobalActionShortcuts to stay under the 1000-line file ceiling.
- Unit tests for ordering/adjacency and an e2e case in
  navigation.spec.ts.

Closes block#3078

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Enzo William <enzowilliam99@gmail.com>
@enzowilliam
enzowilliam requested a review from a team as a code owner July 28, 2026 19:46
@enzowilliam

Copy link
Copy Markdown
Author

Screenshots for the channel-navigation shortcut (captured via the e2e mock bridge, sidebar shots cropped to the sidebar).

Before

#general is the active channel.

01-before-general-selected

After one press of ⌥↓

Selection moves to the next channel in sidebar order, #random.

02-after-next-random-selected

Muted channels are skipped

With #random muted (dimmed), one press from #general lands on #secret-projects.

03-skip-muted-random

Settings → Keyboard Shortcuts

Both shortcuts registered under Navigation.

04-settings-keyboard-shortcuts

wpfleger96 added a commit that referenced this pull request Aug 2, 2026
…and swipe gestures (#3778)

## Problem

Two related gaps in global back/forward navigation. Fixes #3775.

1. The keyboard shortcuts almost never fire in real use — users fall
back to clicking the toolbar chevrons and assume the shortcuts don't
exist.
2. On macOS, mouse back/forward buttons (X1/X2) and horizontal swipe
gestures do nothing, although they navigate in every browser and in
Slack.

**Duplicate check:** searched open PRs and issues — none found beyond
#3775 (filed alongside this fix). #3078 / #3377 are
next/previous-*channel* navigation, a different feature.

## Root causes

**Keyboard:** `useBackForwardControls`'s keydown handler bailed whenever
the event target was editable — but `useComposerAutofocus` deliberately
focuses the message composer (a ProseMirror contenteditable) on mount
and on every channel switch. In steady state focus almost always lives
in the composer, so the chords were silently swallowed. Invisible to CI
because `navigation.spec.ts` only ever clicked the `global-back` /
`global-forward` buttons, never pressed the keys.

**Mouse/swipe:** on macOS, WKWebView never delivers X1/X2 button events
or swipe gestures to the page (Safari handles them natively in the app
layer, not in page JS), and Buzz had no native handler.

## Fix

### Keyboard chords (web layer)

Match the existing platform chord regardless of the event target and
drop the editable-target guard:

- `⌘[` / `⌘]` have no text-editing semantics in macOS text fields, and
the TipTap/StarterKit editor config binds no `Mod-[` / `Mod-]` shortcuts
(checked `useRichTextEditor.ts` — list indentation is Tab/Shift-Tab).
- `preventDefault()` keeps the chord out of the editor — asserted in the
e2e test.

This matches browsers and Slack, where back/forward chords work while a
text field is focused. Chord matching is extracted into a pure helper,
`app/navigation/backForwardChords.ts`, so it can be unit tested;
behavior (bindings, modifier exclusivity, `code`-based matching for
non-US layouts) is unchanged.

### macOS mouse buttons and swipe gestures (native layer)

An NSEvent local monitor in `mouse_nav.rs` catches what the webview
can't see and emits a `mouse-nav` Tauri event to the main window
(`emit_to`, so navigation stays scoped if multi-window ever lands) that
the frontend acts on. Two AppKit event shapes map to navigation:

- `otherMouseUp` with button 3/4 — mice whose X1/X2 buttons arrive as
plain button events. These are swallowed after emitting so nothing
downstream double-handles them.
- `swipe` with a horizontal delta — AppKit's page-swipe gesture
(`swipeWithEvent:`): `deltaX > 0` back, `deltaX < 0` forward. Sent by
mouse drivers that synthesize a page-swipe gesture for the back/forward
buttons instead of button-3/4 events (the hardware this was verified
on). Stock Apple trackpad and Magic Mouse swipes arrive as phased
scroll-wheel events instead, which this PR does not handle — that path
(`ScrollWheel` + `trackSwipeEventWithOptions:`, which also needs
scroll-edge detection) is deferred to a follow-up. Swipes are passed
through (swallowing mid-gesture events could confuse AppKit gesture
tracking).

The swipe path was verified end to end on hardware whose back/forward
buttons emit only swipe gestures, never button-3/4 events — an
instrumented event monitor confirmed the events arrive as
`NSEventType::Swipe` with `deltaX ±1`, and navigation worked after
mapping them.

## Tests

- **13 unit tests** for the web-side chord matcher
(`backForwardChords.test.mjs`): supported chords, modifier exclusivity,
`code` fallback, and preservation of line-editing shortcuts.
- **6 Rust unit tests** for the native mapping helpers (`mouse_nav.rs`):
button 3/4 directions, other buttons ignored, swipe delta sign →
direction, zero-delta (gesture-begin) ignored.
- **e2e regression case** in `navigation.spec.ts`: presses the platform
chord *while the composer is focused* — the missing coverage. Verified
it fails against the pre-fix implementation and passes with the fix.
- Full desktop unit suite: 3832/3832 pass. Full Rust suite (`cargo
test`, buzz-desktop): 1888 passed / 0 failed. `pnpm typecheck`, `biome
check`, `pnpm check`, `cargo fmt --check`, `cargo clippy`: clean (no new
warnings).
- Full Playwright e2e: 958 passed; 6 failures are relay-infrastructure
tests (live relay seeding / relay state seam) that fail identically
without this change — `navigation.spec.ts` is fully green.

## Manual test

1. Open a channel, then another (composer autofocuses on each switch).
2. `⌘[` — returns to the previous channel; `⌘]` — forward again. Typing
`[` / `]` in the composer inserts normally.
3. Mouse back/forward buttons navigate the same way, from anywhere in
the window (verified on macOS on hardware using both event shapes).

## Update — 2026-07-31

Removed the redundant DOM mouse-button handler after verifying it was
unnecessary. The native macOS path remains unchanged and was revalidated
manually.

---------

Signed-off-by: npub1yvnq5equak5errqpku8stskushny9wsvt0fc2ywcpwt79yslwaqswe7tse <23260a641ceda9918c01b70f05c2dc85e642ba0c5bd38511d80b97e2921f7741@buzz.block.builderlab.xyz>
Signed-off-by: Matheus Iser <matheusiser@squareup.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1yvnq5equak5errqpku8stskushny9wsvt0fc2ywcpwt79yslwaqswe7tse <23260a641ceda9918c01b70f05c2dc85e642ba0c5bd38511d80b97e2921f7741@buzz.block.builderlab.xyz>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
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.

feat(desktop): keyboard shortcut to navigate to next/previous channel

1 participant