diff --git a/apps/client/src/desktop.ts b/apps/client/src/desktop.ts
index e77ba845b..b88eb8d4a 100644
--- a/apps/client/src/desktop.ts
+++ b/apps/client/src/desktop.ts
@@ -64,6 +64,9 @@ function initOnElectron() {
if (options.get("nativeTitleBarVisible") !== "true") {
initTitleBarButtons(style, currentWindow);
}
+
+ // Clear navigation history on frontend refresh.
+ currentWindow.webContents.navigationHistory.clear();
}
function initTitleBarButtons(style: CSSStyleDeclaration, currentWindow: Electron.BrowserWindow) {
diff --git a/apps/client/src/layouts/desktop_layout.tsx b/apps/client/src/layouts/desktop_layout.tsx
index 41a6bb68f..e03e73d09 100644
--- a/apps/client/src/layouts/desktop_layout.tsx
+++ b/apps/client/src/layouts/desktop_layout.tsx
@@ -45,6 +45,7 @@ import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
import SpacerWidget from "../widgets/launch_bar/SpacerWidget.jsx";
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
import Breadcrumb from "../widgets/Breadcrumb.jsx";
+import TabHistoryNavigationButtons from "../widgets/TabHistoryNavigationButtons.jsx";
export default class DesktopLayout {
@@ -80,6 +81,7 @@ export default class DesktopLayout {
.class("tab-row-container")
.child(new FlexContainer("row").id("tab-row-left-spacer"))
.optChild(launcherPaneIsHorizontal, )
+ .child()
.child(new TabRowWidget().class("full-width"))
.optChild(customTitleBarButtons, )
.css("height", "40px")
@@ -102,7 +104,12 @@ export default class DesktopLayout {
new FlexContainer("column")
.id("rest-pane")
.css("flex-grow", "1")
- .optChild(!fullWidthTabBar, new FlexContainer("row").child(new TabRowWidget()).optChild(customTitleBarButtons, ).css("height", "40px"))
+ .optChild(!fullWidthTabBar,
+ new FlexContainer("row")
+ .child()
+ .child(new TabRowWidget())
+ .optChild(customTitleBarButtons, )
+ .css("height", "40px"))
.child(
new FlexContainer("row")
.filling()
diff --git a/apps/client/src/services/tree.ts b/apps/client/src/services/tree.ts
index cfa210600..00e88cf6d 100644
--- a/apps/client/src/services/tree.ts
+++ b/apps/client/src/services/tree.ts
@@ -4,6 +4,8 @@ import froca from "./froca.js";
import hoistedNoteService from "./hoisted_note.js";
import appContext from "../components/app_context.js";
+export const NOTE_PATH_TITLE_SEPARATOR = " › ";
+
async function resolveNotePath(notePath: string, hoistedNoteId = "root") {
const runPath = await resolveNotePathToSegments(notePath, hoistedNoteId);
@@ -254,7 +256,7 @@ async function getNotePathTitle(notePath: string) {
const titlePath = await getNotePathTitleComponents(notePath);
- return titlePath.join(" / ");
+ return titlePath.join(NOTE_PATH_TITLE_SEPARATOR);
}
async function getNoteTitleWithPathAsSuffix(notePath: string) {
diff --git a/apps/client/src/stylesheets/theme-next/shell.css b/apps/client/src/stylesheets/theme-next/shell.css
index 7643a02b2..e3e92cd7d 100644
--- a/apps/client/src/stylesheets/theme-next/shell.css
+++ b/apps/client/src/stylesheets/theme-next/shell.css
@@ -502,7 +502,7 @@ div.bookmark-folder-widget .note-link .bx {
font-size: 1.2em;
}
-/*
+/*
* QUICK SEARCH BOX
*/
@@ -613,7 +613,7 @@ div.quick-search .dropdown-menu {
* As a temporary workaround, the backdrop and transparency are disabled for the
* vertical layout.
*/
-body.layout-vertical.background-effects div.quick-search .dropdown-menu {
+body.layout-vertical.background-effects div.quick-search .dropdown-menu {
--menu-background-color: var(--menu-background-color-no-backdrop) !important;
}
@@ -945,12 +945,26 @@ body.electron.background-effects.layout-horizontal .tab-row-container .toggle-bu
position: absolute;
bottom: 0;
inset-inline-start: -10px;
- inset-inline-end: -10px;
+ inset-inline-end: -6px;
top: 32px;
height: 1px;
border-bottom: 1px solid var(--launcher-pane-horiz-border-color);
}
+body.electron.background-effects.layout-horizontal .tab-row-container .tab-history-navigation-buttons {
+ position: relative;
+
+ &:after {
+ content: "";
+ position: absolute;
+ bottom: 0;
+ inset-inline-start: 0;
+ inset-inline-end: -7px;
+ height: 1px;
+ border-bottom: 1px solid var(--launcher-pane-horiz-border-color);
+ }
+}
+
body.electron.background-effects.layout-horizontal .tab-row-container .tab-scroll-button-left,
body.electron.background-effects.layout-horizontal .tab-row-container .tab-scroll-button-right {
position: relative;
@@ -1569,7 +1583,7 @@ div.floating-buttons .show-floating-buttons-button {
div.floating-buttons .show-floating-buttons-button::before {
animation: floating-buttons-show-hide-button-animation 400ms ease-out;
}
-
+
div.floating-buttons .show-floating-buttons-button:hover,
div.floating-buttons .show-floating-buttons-button:active {
box-shadow: var(--floating-button-show-button-hover-shadow);
@@ -1831,7 +1845,7 @@ div.find-replace-widget div.find-widget-found-wrapper > span {
.excalidraw .dropdown-menu {
border: unset !important;
- box-shadow: unset !important;
+ box-shadow: unset !important;
background-color: transparent !important;
--island-bg-color: var(--menu-background-color);
--shadow-island: 0px 10px 20px rgba(0, 0, 0, var(--dropdown-shadow-opacity));
@@ -1850,4 +1864,4 @@ div.find-replace-widget div.find-widget-found-wrapper > span {
.excalidraw .dropdown-menu:before {
content: unset !important;
-}
\ No newline at end of file
+}
diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json
index fea8ca13f..24e30c6a9 100644
--- a/apps/client/src/translations/en/translation.json
+++ b/apps/client/src/translations/en/translation.json
@@ -2117,5 +2117,9 @@
"unknown_http_error_title": "Communication error with the server",
"unknown_http_error_content": "Status code: {{statusCode}}\nURL: {{method}} {{url}}\nMessage: {{message}}",
"traefik_blocks_requests": "If you are using the Traefik reverse proxy, it introduced a breaking change which affects the communication with the server."
+ },
+ "tab_history_navigation_buttons": {
+ "go-back": "Go back to previous note",
+ "go-forward": "Go forward to next note"
}
}
diff --git a/apps/client/src/widgets/TabHistoryNavigationButtons.css b/apps/client/src/widgets/TabHistoryNavigationButtons.css
new file mode 100644
index 000000000..4d040b9a5
--- /dev/null
+++ b/apps/client/src/widgets/TabHistoryNavigationButtons.css
@@ -0,0 +1,7 @@
+.component.tab-history-navigation-buttons {
+ contain: none;
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ margin-inline-end: 0.5em;
+}
diff --git a/apps/client/src/widgets/TabHistoryNavigationButtons.tsx b/apps/client/src/widgets/TabHistoryNavigationButtons.tsx
new file mode 100644
index 000000000..07ecf6b66
--- /dev/null
+++ b/apps/client/src/widgets/TabHistoryNavigationButtons.tsx
@@ -0,0 +1,64 @@
+import "./TabHistoryNavigationButtons.css";
+
+import { useEffect, useMemo, useState } from "preact/hooks";
+
+import { t } from "../services/i18n";
+import { dynamicRequire, isElectron } from "../services/utils";
+import { handleHistoryContextMenu } from "./launch_bar/HistoryNavigation";
+import ActionButton from "./react/ActionButton";
+import { useLauncherVisibility } from "./react/hooks";
+
+export default function TabHistoryNavigationButtons() {
+ const webContents = useMemo(() => isElectron() ? dynamicRequire("@electron/remote").getCurrentWebContents() : undefined, []);
+ const onContextMenu = webContents ? handleHistoryContextMenu(webContents) : undefined;
+ const { canGoBack, canGoForward } = useBackForwardState(webContents);
+ const legacyBackVisible = useLauncherVisibility("_lbBackInHistory");
+ const legacyForwardVisible = useLauncherVisibility("_lbForwardInHistory");
+
+ return (isElectron() &&
+
+ {!legacyBackVisible &&
}
+ {!legacyForwardVisible &&
}
+
+ );
+}
+
+function useBackForwardState(webContents: Electron.WebContents | undefined) {
+ const [ canGoBack, setCanGoBack ] = useState(webContents?.navigationHistory.canGoBack());
+ const [ canGoForward, setCanGoForward ] = useState(webContents?.navigationHistory.canGoForward());
+
+ useEffect(() => {
+ if (!webContents) return;
+
+ const updateNavigationState = () => {
+ setCanGoBack(webContents.navigationHistory.canGoBack());
+ setCanGoForward(webContents.navigationHistory.canGoForward());
+ };
+
+ webContents.on("did-navigate", updateNavigationState);
+ webContents.on("did-navigate-in-page", updateNavigationState);
+
+ return () => {
+ webContents.removeListener("did-navigate", updateNavigationState);
+ webContents.removeListener("did-navigate-in-page", updateNavigationState);
+ };
+ }, [ webContents ]);
+
+ if (!webContents) {
+ return { canGoBack: true, canGoForward: true };
+ }
+
+ return { canGoBack, canGoForward };
+}
diff --git a/apps/client/src/widgets/launch_bar/HistoryNavigation.tsx b/apps/client/src/widgets/launch_bar/HistoryNavigation.tsx
index f9ea51c57..3e0ce6f96 100644
--- a/apps/client/src/widgets/launch_bar/HistoryNavigation.tsx
+++ b/apps/client/src/widgets/launch_bar/HistoryNavigation.tsx
@@ -1,11 +1,13 @@
-import { useEffect, useRef } from "preact/hooks";
+import type { WebContents } from "electron";
+import { useMemo } from "preact/hooks";
+
import FNote from "../../entities/fnote";
+import contextMenu, { MenuCommandItem } from "../../menus/context_menu";
+import froca from "../../services/froca";
+import link from "../../services/link";
+import tree from "../../services/tree";
import { dynamicRequire, isElectron } from "../../services/utils";
import { LaunchBarActionButton, useLauncherIconAndTitle } from "./launch_bar_widgets";
-import type { WebContents } from "electron";
-import contextMenu, { MenuCommandItem } from "../../menus/context_menu";
-import tree from "../../services/tree";
-import link from "../../services/link";
interface HistoryNavigationProps {
launcherNote: FNote;
@@ -16,71 +18,64 @@ 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(() => isElectron() ? dynamicRequire("@electron/remote").getCurrentWebContents() : undefined, []);
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={webContents ? handleHistoryContextMenu(webContents) : undefined}
/>
- )
+ );
+}
+
+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 { noteId, notePath } = link.parseNavigationStateFromUrl(history[idx].url);
+ if (!noteId || !notePath) continue;
+
+ const title = await tree.getNotePathTitle(notePath);
+ const index = parseInt(idx, 10);
+ const note = froca.getNoteFromCache(noteId);
+
+ items.push({
+ title,
+ command: idx,
+ checked: index === activeIndex,
+ enabled: index !== activeIndex,
+ uiIcon: note?.getIcon()
+ });
+ }
+
+ 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);
+ }
+ }
+ });
+ };
}
diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx
index 0c9883df2..fbcd7095e 100644
--- a/apps/client/src/widgets/react/hooks.tsx
+++ b/apps/client/src/widgets/react/hooks.tsx
@@ -897,7 +897,31 @@ export function useChildNotes(parentNoteId: string | undefined) {
}
setChildNotes(childNotes ?? []);
})();
- }, [ parentNoteId ]);
+ }, [ parentNoteId ]);
return childNotes;
}
+
+export function useLauncherVisibility(launchNoteId: string) {
+ const checkIfVisible = useCallback(() => {
+ const note = froca.getNoteFromCache(launchNoteId);
+ return note?.getParentBranches().some(branch =>
+ [ "_lbVisibleLaunchers", "_lbMobileVisibleLaunchers" ].includes(branch.parentNoteId)) ?? false;
+ }, [ launchNoteId ]);
+
+ const [ isVisible, setIsVisible ] = useState(checkIfVisible());
+
+ // React to note not being available in the cache.
+ useEffect(() => {
+ froca.getNote(launchNoteId).then(() => setIsVisible(checkIfVisible()));
+ }, [ launchNoteId, checkIfVisible ]);
+
+ // React to changes.
+ useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
+ if (loadResults.getBranchRows().some(branch => branch.noteId === launchNoteId)) {
+ setIsVisible(checkIfVisible());
+ }
+ });
+
+ return isVisible;
+}
diff --git a/apps/client/src/widgets/ribbon/NotePathsTab.tsx b/apps/client/src/widgets/ribbon/NotePathsTab.tsx
index 00249b0b4..0b997cfb2 100644
--- a/apps/client/src/widgets/ribbon/NotePathsTab.tsx
+++ b/apps/client/src/widgets/ribbon/NotePathsTab.tsx
@@ -6,6 +6,7 @@ import { useEffect, useMemo, useState } from "preact/hooks";
import { NotePathRecord } from "../../entities/fnote";
import NoteLink from "../react/NoteLink";
import { joinElements } from "../react/react_utils";
+import { NOTE_PATH_TITLE_SEPARATOR } from "../../services/tree";
export default function NotePathsTab({ note, hoistedNoteId, notePath }: TabContext) {
const [ sortedNotePaths, setSortedNotePaths ] = useState();
@@ -33,7 +34,7 @@ export default function NotePathsTab({ note, hoistedNoteId, notePath }: TabConte
{sortedNotePaths?.length ? t("note_paths.intro_placed") : t("note_paths.intro_not_placed")}
-
+
{sortedNotePaths?.length ? sortedNotePaths.map(sortedNotePath => (
)) : undefined}
-
+