From dfe0d1bc767fd74614e41a8c9211327fc496fb76 Mon Sep 17 00:00:00 2001 From: Noah Date: Tue, 21 Jul 2026 16:13:41 -0400 Subject: [PATCH] refactor(desktop): simplify app shell shortcuts Extract global navigation shortcuts into a focused hook and remove the unused threadActivityItems context field. Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-019f8626-f5ed-731e-94dc-37ce0fd51b93 --- desktop/src/app/AppShell.tsx | 75 ++++---------------------- desktop/src/app/AppShellContext.tsx | 3 -- desktop/src/app/useGlobalShortcuts.ts | 78 +++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 68 deletions(-) create mode 100644 desktop/src/app/useGlobalShortcuts.ts diff --git a/desktop/src/app/AppShell.tsx b/desktop/src/app/AppShell.tsx index 369241bac..d7bd9fbf7 100644 --- a/desktop/src/app/AppShell.tsx +++ b/desktop/src/app/AppShell.tsx @@ -18,6 +18,7 @@ import { useAppShellLifecycleEffects } from "@/app/useAppShellLifecycleEffects"; import { useThreadActivityFeedItems } from "@/app/useThreadActivityFeedItems"; import { useTauriWindowDrag } from "@/app/useTauriWindowDrag"; import { useWebviewZoomShortcuts } from "@/app/useWebviewZoomShortcuts"; +import { useGlobalShortcuts } from "@/app/useGlobalShortcuts"; import { channelsQueryKey, useChannelsQuery, @@ -84,7 +85,6 @@ import { ChannelNavigationProvider } from "@/shared/context/ChannelNavigationCon import { MainInsetProvider } from "@/shared/layout/MainInsetContext"; import { chromeCssVarDefaults } from "@/shared/layout/chromeLayout"; import { cn } from "@/shared/lib/cn"; -import { hasPrimaryShortcutModifier } from "@/shared/lib/platform"; import { useMessageDeepLinks } from "@/shared/useMessageDeepLinks"; import { SidebarInset, SidebarProvider } from "@/shared/ui/sidebar"; import { RelayConnectionOverlay } from "@/app/RelayConnectionOverlay"; @@ -584,69 +584,15 @@ export function AppShell() { () => setIsCreateChannelOpen(true), [], ); - React.useLayoutEffect(() => { - if (settingsOpen) { - return; - } - - function handleKeyDown(event: KeyboardEvent) { - if (!hasPrimaryShortcutModifier(event) || event.altKey || event.repeat) { - return; - } - - // A focused surface may claim the shortcut first — e.g. the composer - // consumes ⌘K to open the link editor when text is selected. Its - // element-level handler runs before this window-level bubble listener - // and calls `preventDefault()`; respect that instead of also opening - // the global dialog. - if (event.defaultPrevented) { - return; - } - - const key = event.key.toLowerCase(); - if (key === "k" && !event.shiftKey) { - event.preventDefault(); - handleOpenSearch(); - return; - } - - if (key === "k" && event.shiftKey) { - event.preventDefault(); - handleOpenNewDm(); - return; - } - - if (key === "n" && event.shiftKey) { - event.preventDefault(); - handleOpenCreateChannel(); - return; - } - - if (key === "o" && event.shiftKey) { - event.preventDefault(); - handleOpenBrowseChannels(); - return; - } - - if (key === "a" && event.shiftKey) { - event.preventDefault(); - void goHome(); - return; - } - } - - window.addEventListener("keydown", handleKeyDown); - return () => { - window.removeEventListener("keydown", handleKeyDown); - }; - }, [ - handleOpenBrowseChannels, - handleOpenNewDm, - handleOpenCreateChannel, - handleOpenSearch, - goHome, - settingsOpen, - ]); + const handleGoHome = React.useCallback(() => void goHome(), [goHome]); + useGlobalShortcuts({ + disabled: settingsOpen, + onOpenSearch: handleOpenSearch, + onOpenNewDm: handleOpenNewDm, + onOpenCreateChannel: handleOpenCreateChannel, + onBrowseChannels: handleOpenBrowseChannels, + onGoHome: handleGoHome, + }); useSettingsShortcuts({ onClose: handleCloseSettings, onOpenSettings: handleOpenSettings, @@ -688,7 +634,6 @@ export function AppShell() { isFollowingThread, isNotifiedForThread, isThreadMuted: (rootId) => mutedRootIds.has(rootId), - threadActivityItems, threadActivityFeedItems, feedItemState, onOpenSettings: handleOpenSettings, diff --git a/desktop/src/app/AppShellContext.tsx b/desktop/src/app/AppShellContext.tsx index 4a64de0cb..e6b6f726b 100644 --- a/desktop/src/app/AppShellContext.tsx +++ b/desktop/src/app/AppShellContext.tsx @@ -1,6 +1,5 @@ import * as React from "react"; import type { ContextParentResolver } from "@/features/channels/readState/readStateManager"; -import type { ThreadActivityItem } from "@/features/channels/useUnreadChannels"; import type { FeedItemState } from "@/features/home/useFeedItemState"; import type { FeedItem } from "@/shared/api/types"; import type { SettingsSection } from "@/features/settings/ui/SettingsPanels"; @@ -44,7 +43,6 @@ type AppShellContextValue = { isFollowingThread: (rootId: string) => boolean; isNotifiedForThread: (rootId: string) => boolean; isThreadMuted: (rootId: string) => boolean; - threadActivityItems: ThreadActivityItem[]; threadActivityFeedItems: FeedItem[]; feedItemState: FeedItemState; // Open the Settings panel at the given section. Available on all surfaces @@ -72,7 +70,6 @@ const AppShellContext = React.createContext({ isFollowingThread: () => false, isNotifiedForThread: () => false, isThreadMuted: () => false, - threadActivityItems: [], threadActivityFeedItems: [], feedItemState: { doneSet: EMPTY_SET, diff --git a/desktop/src/app/useGlobalShortcuts.ts b/desktop/src/app/useGlobalShortcuts.ts new file mode 100644 index 000000000..d4aa3cc1d --- /dev/null +++ b/desktop/src/app/useGlobalShortcuts.ts @@ -0,0 +1,78 @@ +import * as React from "react"; + +import { hasPrimaryShortcutModifier } from "@/shared/lib/platform"; + +type UseGlobalShortcutsOptions = { + /** Whether global navigation shortcuts are temporarily disabled. */ + disabled: boolean; + /** ⌘K — open the search / quick-switch dialog. */ + onOpenSearch: () => void; + /** ⇧⌘K — open the new direct-message composer. */ + onOpenNewDm: () => void; + /** ⇧⌘N — open the create-channel dialog. */ + onOpenCreateChannel: () => void; + /** ⇧⌘O — open the channel browser. */ + onBrowseChannels: () => void; + /** ⇧⌘A — navigate to the home feed. */ + onGoHome: () => void; +}; + +/** + * Window-level global navigation shortcuts. Dormant while `disabled` (e.g. when + * settings is open). Respects `event.defaultPrevented` so a focused surface — + * e.g. the composer consuming ⌘K for its link editor — can claim the key first + * via an element-level handler that runs before this bubble listener. + */ +export function useGlobalShortcuts({ + disabled, + onOpenSearch, + onOpenNewDm, + onOpenCreateChannel, + onBrowseChannels, + onGoHome, +}: UseGlobalShortcutsOptions): void { + React.useLayoutEffect(() => { + if (disabled) { + return; + } + + function handleKeyDown(event: KeyboardEvent) { + if (!hasPrimaryShortcutModifier(event) || event.altKey || event.repeat) { + return; + } + if (event.defaultPrevented) { + return; + } + + const key = event.key.toLowerCase(); + if (key === "k" && !event.shiftKey) { + event.preventDefault(); + onOpenSearch(); + } else if (key === "k" && event.shiftKey) { + event.preventDefault(); + onOpenNewDm(); + } else if (key === "n" && event.shiftKey) { + event.preventDefault(); + onOpenCreateChannel(); + } else if (key === "o" && event.shiftKey) { + event.preventDefault(); + onBrowseChannels(); + } else if (key === "a" && event.shiftKey) { + event.preventDefault(); + onGoHome(); + } + } + + window.addEventListener("keydown", handleKeyDown); + return () => { + window.removeEventListener("keydown", handleKeyDown); + }; + }, [ + disabled, + onBrowseChannels, + onGoHome, + onOpenCreateChannel, + onOpenNewDm, + onOpenSearch, + ]); +}