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@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019f8626-f5ed-731e-94dc-37ce0fd51b93
This commit is contained in:
Noah
2026-07-21 16:25:50 -04:00
co-authored by Amp
parent 3afc9dae15
commit dfe0d1bc76
3 changed files with 88 additions and 68 deletions
+10 -65
View File
@@ -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,
-3
View File
@@ -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<AppShellContextValue>({
isFollowingThread: () => false,
isNotifiedForThread: () => false,
isThreadMuted: () => false,
threadActivityItems: [],
threadActivityFeedItems: [],
feedItemState: {
doneSet: EMPTY_SET,
+78
View File
@@ -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,
]);
}