From 428d98c6d62e1703fe0c1c2f35e8486cdbe6e9b8 Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Wed, 26 Aug 2026 15:32:49 +1000 Subject: [PATCH] feat(desktop): add back history menu Co-authored-by: Jitter Signed-off-by: Matt Toohey --- desktop/src/app/AppShell.tsx | 11 +- desktop/src/app/AppTopChrome.tsx | 134 ++++++++++++++++-- .../app/navigation/navigationGuard.test.mjs | 32 ++++- desktop/src/app/navigation/navigationGuard.ts | 17 +++ .../app/navigation/navigationHistory.test.mjs | 80 +++++++++++ .../src/app/navigation/navigationHistory.ts | 85 +++++++++++ .../app/navigation/useBackForwardControls.ts | 82 ++++++++--- desktop/tests/e2e/navigation.spec.ts | 45 ++++++ 8 files changed, 449 insertions(+), 37 deletions(-) create mode 100644 desktop/src/app/navigation/navigationHistory.test.mjs create mode 100644 desktop/src/app/navigation/navigationHistory.ts diff --git a/desktop/src/app/AppShell.tsx b/desktop/src/app/AppShell.tsx index eb5ab5a95d..e5a647fde5 100644 --- a/desktop/src/app/AppShell.tsx +++ b/desktop/src/app/AppShell.tsx @@ -13,6 +13,7 @@ import { TerminalContextOverrideProvider, } from "@/app/TerminalContextOverrideContext"; import { useAppNavigation } from "@/app/navigation/useAppNavigation"; +import { describeHistoryLocation } from "@/app/navigation/navigationHistory"; import { useBackForwardControls } from "@/app/navigation/useBackForwardControls"; import { useCommunityNavigationTransitions } from "@/app/useCommunityNavigationTransitions"; import { useLiveHomeFeedActions } from "@/app/useLiveHomeFeedActions"; @@ -141,6 +142,8 @@ export function AppShell() { const mainInsetRef = React.useRef(null); const location = useLocation(); const queryClient = useQueryClient(); + const channelsQuery = useChannelsQuery(); + const channels = channelsQuery.data ?? []; useManagedAgentRuntimeReconciliation(communitiesHook.communities); // sync storage snapshot const { goAgents, @@ -154,8 +157,8 @@ export function AppShell() { closeSettings, openSearchHit, } = useAppNavigation(); - const { canGoBack, canGoForward, goBack, goForward } = - useBackForwardControls(); + const { backHistory, canGoBack, canGoForward, goBack, goBackTo, goForward } = + useBackForwardControls(describeHistoryLocation(location, channels)); const { selectedChannelId, selectedView } = React.useMemo( () => deriveShellRoute(location.pathname), [location.pathname], @@ -233,8 +236,6 @@ export function AppShell() { const { feedProfilesQuery, homeFeedQuery, notificationSettings } = useHomeFeedNotifications(identityQuery.data?.pubkey); const feedItemState = useFeedItemState(identityQuery.data?.pubkey); - const channelsQuery = useChannelsQuery(); - const channels = channelsQuery.data ?? []; useReminderNotifications( identityQuery.data?.pubkey, notificationSettings.settings, @@ -774,10 +775,12 @@ export function AppShell() { {!settingsOpen && !isHuddleRoom ? ( ) : null} diff --git a/desktop/src/app/AppTopChrome.tsx b/desktop/src/app/AppTopChrome.tsx index 35b5e4b209..f58cf1e6fb 100644 --- a/desktop/src/app/AppTopChrome.tsx +++ b/desktop/src/app/AppTopChrome.tsx @@ -1,18 +1,27 @@ import * as React from "react"; import { ChevronLeft, ChevronRight } from "lucide-react"; +import type { BackHistoryEntry } from "@/app/navigation/navigationHistory"; import { isMacPlatform } from "@/shared/lib/platform"; import { useIsFullscreen } from "@/shared/lib/useIsFullscreen"; import { Button } from "@/shared/ui/button"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger, +} from "@/shared/ui/context-menu"; import { DrawerPanelIcon } from "@/shared/ui/DrawerPanelIcon"; import { cn } from "@/shared/lib/cn"; import { topChromeBackdrop } from "@/shared/layout/chromeLayout"; import { useOptionalSidebar } from "@/shared/ui/sidebar"; type AppTopChromeProps = { + backHistory: BackHistoryEntry[]; canGoBack: boolean; canGoForward: boolean; onGoBack: () => void; + onGoBackTo: (index: number) => void; onGoForward: () => void; hasCommunityRail?: boolean; }; @@ -25,6 +34,7 @@ const TOP_CHROME_ICON_BUTTON_CLASS = "h-[28px] w-[28px] rounded-[4px] text-sidebar-foreground/65 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"; const HISTORY_ICON_BUTTON_CLASS = "h-[28px] w-[24px] rounded-[4px] text-sidebar-foreground/65 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [&_svg]:size-[16px]"; +const BACK_HISTORY_LONG_PRESS_MS = 500; function preventTopChromeWheel(event: WheelEvent) { event.preventDefault(); @@ -52,10 +62,117 @@ function TopChromeSidebarTrigger() { ); } +function BackHistoryButton({ + backHistory, + canGoBack, + onGoBack, + onGoBackTo, +}: Pick< + AppTopChromeProps, + "backHistory" | "canGoBack" | "onGoBack" | "onGoBackTo" +>) { + const buttonRef = React.useRef(null); + const longPressTimerRef = React.useRef(null); + const longPressTriggeredRef = React.useRef(false); + + const cancelLongPress = React.useCallback(() => { + if (longPressTimerRef.current !== null) { + window.clearTimeout(longPressTimerRef.current); + longPressTimerRef.current = null; + } + }, []); + + React.useEffect(() => cancelLongPress, [cancelLongPress]); + + const handlePointerDown = React.useCallback( + (event: React.PointerEvent) => { + if (event.button !== 0 || backHistory.length === 0) { + return; + } + + cancelLongPress(); + longPressTriggeredRef.current = false; + const { clientX, clientY } = event; + longPressTimerRef.current = window.setTimeout(() => { + longPressTimerRef.current = null; + longPressTriggeredRef.current = true; + buttonRef.current?.dispatchEvent( + new MouseEvent("contextmenu", { + bubbles: true, + button: 2, + cancelable: true, + clientX, + clientY, + view: window, + }), + ); + }, BACK_HISTORY_LONG_PRESS_MS); + }, + [backHistory.length, cancelLongPress], + ); + + return ( + { + if (!open) { + longPressTriggeredRef.current = false; + } + }} + > + + + + {backHistory.length > 0 ? ( + + {backHistory.map((entry) => ( + onGoBackTo(entry.index)} + > + {entry.label} + + ))} + + ) : null} + + ); +} + export function AppTopChrome({ + backHistory, canGoBack, canGoForward, onGoBack, + onGoBackTo, onGoForward, hasCommunityRail = false, }: AppTopChromeProps) { @@ -132,17 +249,12 @@ export function AppTopChrome({ >
- +