diff --git a/apps/client/src/widgets/layout/Breadcrumb.tsx b/apps/client/src/widgets/layout/Breadcrumb.tsx index 085b92ef7..7278b403b 100644 --- a/apps/client/src/widgets/layout/Breadcrumb.tsx +++ b/apps/client/src/widgets/layout/Breadcrumb.tsx @@ -11,7 +11,7 @@ import froca from "../../services/froca"; import ActionButton from "../react/ActionButton"; import Dropdown from "../react/Dropdown"; import { FormListItem } from "../react/FormList"; -import { useChildNotes, useNoteLabel, useNoteProperty } from "../react/hooks"; +import { useChildNotes, useNote, useNoteLabel, useNoteProperty } from "../react/hooks"; import Icon from "../react/Icon"; import NoteLink from "../react/NoteLink"; @@ -57,7 +57,7 @@ export default function Breadcrumb({ note, noteContext }: { note: FNote, noteCon } function BreadcrumbRoot({ noteContext }: { noteContext: NoteContext | undefined }) { - const note = useMemo(() => froca.getNoteFromCache("root"), []); + const note = useNote(noteContext?.hoistedNoteId); useNoteLabel(note, "iconClass"); const title = useNoteProperty(note, "title"); @@ -66,7 +66,7 @@ function BreadcrumbRoot({ noteContext }: { noteContext: NoteContext | undefined className="root-note" icon={note.getIcon()} text={title ?? ""} - onClick={() => noteContext?.setNote("root")} + onClick={() => noteContext?.setNote(note.noteId)} onContextMenu={(e) => { e.preventDefault(); link_context_menu.openContextMenu(note.noteId, e); diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index 0b9546694..26b699e1e 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -1,29 +1,30 @@ -import { CSSProperties } from "preact/compat"; -import { DragData } from "../note_tree"; import { FilterLabelsByType, KeyboardActionNames, OptionNames, RelationNames } from "@triliumnext/commons"; -import { MutableRef, useCallback, useContext, useDebugValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks"; -import { NoteContextContext, ParentComponent, refToJQuerySelector } from "./react_utils"; -import { RefObject, VNode } from "preact"; import { Tooltip } from "bootstrap"; -import { ViewMode, ViewScope } from "../../services/link"; +import Mark from "mark.js"; +import { RefObject, VNode } from "preact"; +import { CSSProperties } from "preact/compat"; +import { MutableRef, useCallback, useContext, useDebugValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks"; + import appContext, { EventData, EventNames } from "../../components/app_context"; -import attributes from "../../services/attributes"; -import BasicWidget, { ReactWrappedWidget } from "../basic_widget"; import Component from "../../components/component"; +import NoteContext from "../../components/note_context"; import FBlob from "../../entities/fblob"; import FNote from "../../entities/fnote"; +import attributes from "../../services/attributes"; +import froca from "../../services/froca"; import keyboard_actions from "../../services/keyboard_actions"; -import Mark from "mark.js"; -import NoteContext from "../../components/note_context"; -import NoteContextAwareWidget from "../note_context_aware_widget"; +import { ViewScope } from "../../services/link"; import options, { type OptionValue } from "../../services/options"; import protected_session_holder from "../../services/protected_session_holder"; +import server from "../../services/server"; +import shortcuts, { Handler, removeIndividualBinding } from "../../services/shortcuts"; import SpacedUpdate from "../../services/spaced_update"; import toast, { ToastOptions } from "../../services/toast"; import utils, { escapeRegExp, randomString, reloadFrontendApp } from "../../services/utils"; -import server from "../../services/server"; -import shortcuts, { Handler, removeIndividualBinding } from "../../services/shortcuts"; -import froca from "../../services/froca"; +import BasicWidget, { ReactWrappedWidget } from "../basic_widget"; +import NoteContextAwareWidget from "../note_context_aware_widget"; +import { DragData } from "../note_tree"; +import { NoteContextContext, ParentComponent, refToJQuerySelector } from "./react_utils"; export function useTriliumEvent(eventName: T, handler: (data: EventData) => void) { const parentComponent = useContext(ParentComponent); @@ -42,7 +43,7 @@ export function useTriliumEvents(eventNames: T[], handler: for (const eventName of eventNames) { handlers.push({ eventName, callback: (data) => { handler(data, eventName); - }}) + }}); } for (const { eventName, callback } of handlers) { @@ -111,8 +112,8 @@ export function useEditorSpacedUpdate({ note, noteContext, getData, onContentCha await server.put(`notes/${note.noteId}/data`, data, parentComponent?.componentId); dataSaved?.(data); - } - }, [ note, getData, dataSaved ]) + }; + }, [ note, getData, dataSaved ]); const spacedUpdate = useSpacedUpdate(callback); // React to note/blob changes. @@ -137,7 +138,7 @@ export function useEditorSpacedUpdate({ note, noteContext, getData, onContentCha useTriliumEvent("beforeNoteContextRemove", async ({ ntxIds }) => { if (!noteContext?.ntxId || !ntxIds.includes(noteContext.ntxId)) return; await spacedUpdate.updateNowIfNecessary(); - }) + }); // Save if needed upon window/browser closing. useEffect(() => { @@ -170,7 +171,7 @@ export function useTriliumOption(name: OptionNames, needsRefresh?: boolean): [st if (needsRefresh) { reloadFrontendApp(`option change: ${name}`); } - } + }; }, [ name, needsRefresh ]); useTriliumEvent("entitiesReloaded", useCallback(({ loadResults }) => { @@ -178,14 +179,14 @@ export function useTriliumOption(name: OptionNames, needsRefresh?: boolean): [st const newValue = options.get(name); setValue(newValue); } - }, [ name, setValue ])); + }, [ name, setValue ])); useDebugValue(name); return [ value, wrappedSetValue - ] + ]; } /** @@ -266,7 +267,7 @@ export function useTriliumOptions(...names: T[]) { * @returns a name with the given prefix and a random alpanumeric string appended to it. */ export function useUniqueName(prefix?: string) { - return useMemo(() => (prefix ? prefix + "-" : "") + utils.randomString(10), [ prefix ]); + return useMemo(() => (prefix ? `${prefix }-` : "") + utils.randomString(10), [ prefix ]); } export function useNoteContext() { @@ -428,7 +429,7 @@ export function useNoteRelation(note: FNote | undefined | null, relationName: Re const setter = useCallback((value: string | undefined) => { if (note) { - attributes.setAttribute(note, "relation", relationName, value) + attributes.setAttribute(note, "relation", relationName, value); } }, [note]); @@ -478,7 +479,7 @@ export function useNoteLabel(note: FNote | undefined | null, labelName: FilterLa const setter = useCallback((value: string | null | undefined) => { if (note) { if (value !== null) { - attributes.setLabel(note.noteId, labelName, value) + attributes.setLabel(note.noteId, labelName, value); } else { attributes.removeOwnedLabelByName(note, labelName); } @@ -537,7 +538,7 @@ export function useNoteLabelInt(note: FNote | undefined | null, labelName: Filte return [ (value ? parseInt(value, 10) : undefined), (newValue) => setValue(String(newValue)) - ] + ]; } export function useNoteBlob(note: FNote | null | undefined, componentId?: string): FBlob | null | undefined { @@ -554,7 +555,7 @@ export function useNoteBlob(note: FNote | null | undefined, componentId?: string } } - useEffect(() => { refresh() }, [ note?.noteId ]); + useEffect(() => { refresh(); }, [ note?.noteId ]); useTriliumEvent("entitiesReloaded", ({ loadResults }) => { if (!note) return; @@ -616,7 +617,7 @@ export function useLegacyWidget(widgetFactory: () => T, { useDebugValue(widget); - return [
, widget ] + return [
, widget ]; } /** @@ -643,7 +644,7 @@ export function useElementSize(ref: RefObject) { return () => { resizeObserver.unobserve(element); resizeObserver.disconnect(); - } + }; }, [ ref ]); return size; @@ -703,7 +704,7 @@ export function useTooltip(elRef: RefObject, config: Partial(); +const tooltips = new Set(); /** * Similar to {@link useTooltip}, but doesn't expose methods to imperatively hide or show the tooltip. @@ -732,7 +733,7 @@ export function useStaticTooltip(elRef: RefObject, config?: Partial { if (!highlightedTokens?.length) return null; const regex = highlightedTokens.map((token) => escapeRegExp(token)).join("|"); - return new RegExp(regex, "gi") + return new RegExp(regex, "gi"); }, [ highlightedTokens ]); return (el: HTMLElement | null | undefined) => { @@ -842,7 +843,7 @@ export function useNoteTreeDrag(containerRef: MutableRef { container.removeEventListener("dragenter", onDragEnter); @@ -878,7 +879,7 @@ export function useKeyboardShortcuts(scope: "code-detail" | "text-detail", conta for (const binding of bindings) { removeIndividualBinding(binding); } - } + }; }, [ scope, containerRef, parentComponent, ntxId ]); } @@ -986,3 +987,40 @@ export function useLauncherVisibility(launchNoteId: string) { return isVisible; } + +export function useNote(noteId: string | null | undefined, silentNotFoundError = false) { + const [ note, setNote ] = useState(noteId ? froca.getNoteFromCache(noteId) : undefined); + const requestIdRef = useRef(0); + + useEffect(() => { + if (!noteId) { + setNote(undefined); + return; + } + + if (note?.noteId === noteId) { + return; + } + + // Try to read from cache. + const cachedNote = froca.getNoteFromCache(noteId); + if (cachedNote) { + setNote(cachedNote); + return; + } + + // Read it asynchronously. + const requestId = ++requestIdRef.current; + froca.getNote(noteId, silentNotFoundError).then(readNote => { + // Only update if this is the latest request. + if (readNote && requestId === requestIdRef.current) { + setNote(readNote); + } + }); + }, [ note, noteId, silentNotFoundError ]); + + if (note?.noteId === noteId) { + return note; + } + return undefined; +}