From 9e099444b6478e40abb7390b90573de547eda81f Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 9 Dec 2025 14:23:06 +0200 Subject: [PATCH] feat(tab_navigation): functional context menu --- .../widgets/TabHistoryNavigationButtons.tsx | 8 ++ .../widgets/launch_bar/HistoryNavigation.tsx | 125 +++++++++--------- 2 files changed, 68 insertions(+), 65 deletions(-) diff --git a/apps/client/src/widgets/TabHistoryNavigationButtons.tsx b/apps/client/src/widgets/TabHistoryNavigationButtons.tsx index e3972f0df..d0e6afced 100644 --- a/apps/client/src/widgets/TabHistoryNavigationButtons.tsx +++ b/apps/client/src/widgets/TabHistoryNavigationButtons.tsx @@ -2,19 +2,27 @@ import "./TabHistoryNavigationButtons.css"; import { t } from "../services/i18n"; import ActionButton from "./react/ActionButton"; +import { useCallback, useMemo } from "preact/hooks"; +import { handleHistoryContextMenu } from "./launch_bar/HistoryNavigation"; +import { dynamicRequire } from "../services/utils"; export default function TabHistoryNavigationButtons() { + const webContents = useMemo(() => dynamicRequire("@electron/remote").getCurrentWebContents(), []); + const onContextMenu = handleHistoryContextMenu(webContents); + return (
); diff --git a/apps/client/src/widgets/launch_bar/HistoryNavigation.tsx b/apps/client/src/widgets/launch_bar/HistoryNavigation.tsx index f9ea51c57..e861e3358 100644 --- a/apps/client/src/widgets/launch_bar/HistoryNavigation.tsx +++ b/apps/client/src/widgets/launch_bar/HistoryNavigation.tsx @@ -1,11 +1,12 @@ -import { useEffect, useRef } from "preact/hooks"; -import FNote from "../../entities/fnote"; -import { dynamicRequire, isElectron } from "../../services/utils"; -import { LaunchBarActionButton, useLauncherIconAndTitle } from "./launch_bar_widgets"; import type { WebContents } from "electron"; +import { useCallback, useMemo } from "preact/hooks"; + +import FNote from "../../entities/fnote"; import contextMenu, { MenuCommandItem } from "../../menus/context_menu"; -import tree from "../../services/tree"; import link from "../../services/link"; +import tree from "../../services/tree"; +import { dynamicRequire } from "../../services/utils"; +import { LaunchBarActionButton, useLauncherIconAndTitle } from "./launch_bar_widgets"; interface HistoryNavigationProps { launcherNote: FNote; @@ -16,71 +17,65 @@ const HISTORY_LIMIT = 20; export default function HistoryNavigationButton({ launcherNote, command }: HistoryNavigationProps) { const { icon, title } = useLauncherIconAndTitle(launcherNote); - const webContentsRef = useRef(null); - - useEffect(() => { - if (isElectron()) { - const webContents = dynamicRequire("@electron/remote").getCurrentWebContents(); - // without this, the history is preserved across frontend reloads - webContents?.clearHistory(); - webContentsRef.current = webContents; - } - }, []); + const webContents = useMemo(() => dynamicRequire("@electron/remote").getCurrentWebContents(), []); return ( { - e.preventDefault(); - - const webContents = webContentsRef.current; - if (!webContents || webContents.navigationHistory.length() < 2) { - return; - } - - let items: MenuCommandItem[] = []; - - const history = webContents.navigationHistory.getAllEntries(); - const activeIndex = webContents.navigationHistory.getActiveIndex(); - - for (const idx in history) { - const { notePath } = link.parseNavigationStateFromUrl(history[idx].url); - if (!notePath) continue; - - const title = await tree.getNotePathTitle(notePath); - - items.push({ - title, - command: idx, - uiIcon: - parseInt(idx) === activeIndex - ? "bx bx-radio-circle-marked" // compare with type coercion! - : parseInt(idx) < activeIndex - ? "bx bx-left-arrow-alt" - : "bx bx-right-arrow-alt" - }); - } - - items.reverse(); - - if (items.length > HISTORY_LIMIT) { - items = items.slice(0, HISTORY_LIMIT); - } - - contextMenu.show({ - x: e.pageX, - y: e.pageY, - items, - selectMenuItemHandler: (item: MenuCommandItem) => { - if (item && item.command && webContents) { - const idx = parseInt(item.command, 10); - webContents.navigationHistory.goToIndex(idx); - } - } - }); - }} + onContextMenu={handleHistoryContextMenu(webContents)} /> - ) + ); +} + +export function handleHistoryContextMenu(webContents: WebContents) { + return async (e: MouseEvent) => { + e.preventDefault(); + + if (!webContents || webContents.navigationHistory.length() < 2) { + return; + } + + let items: MenuCommandItem[] = []; + + const history = webContents.navigationHistory.getAllEntries(); + const activeIndex = webContents.navigationHistory.getActiveIndex(); + + for (const idx in history) { + const { notePath } = link.parseNavigationStateFromUrl(history[idx].url); + if (!notePath) continue; + + const title = await tree.getNotePathTitle(notePath); + + items.push({ + title, + command: idx, + uiIcon: + parseInt(idx, 10) === activeIndex + ? "bx bx-radio-circle-marked" // compare with type coercion! + : parseInt(idx, 10) < activeIndex + ? "bx bx-left-arrow-alt" + : "bx bx-right-arrow-alt" + }); + } + + items.reverse(); + + if (items.length > HISTORY_LIMIT) { + items = items.slice(0, HISTORY_LIMIT); + } + + contextMenu.show({ + x: e.pageX, + y: e.pageY, + items, + selectMenuItemHandler: (item: MenuCommandItem) => { + if (item && item.command && webContents) { + const idx = parseInt(item.command, 10); + webContents.navigationHistory.goToIndex(idx); + } + } + }); + }; }