diff --git a/apps/client/src/widgets/react/NoteStore.ts b/apps/client/src/widgets/react/NoteStore.ts new file mode 100644 index 000000000..6f34c6d23 --- /dev/null +++ b/apps/client/src/widgets/react/NoteStore.ts @@ -0,0 +1,33 @@ +type Listener = () => void; + +class NoteSavedDataStore { + private data = new Map(); + private listeners = new Map>(); + + get(noteId: string) { + return this.data.get(noteId); + } + + set(noteId: string, value: string) { + this.data.set(noteId, value); + this.listeners.get(noteId)?.forEach(l => l()); + } + + subscribe(noteId: string, listener: Listener) { + let set = this.listeners.get(noteId); + if (!set) { + set = new Set(); + this.listeners.set(noteId, set); + } + set.add(listener); + + return () => { + set!.delete(listener); + if (set!.size === 0) { + this.listeners.delete(noteId); + } + }; + } +} + +export const noteSavedDataStore = new NoteSavedDataStore(); diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index e43ee4c8d..0a9177209 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -2,7 +2,7 @@ import { FilterLabelsByType, KeyboardActionNames, OptionNames, RelationNames } f import { Tooltip } from "bootstrap"; import Mark from "mark.js"; import { RefObject, VNode } from "preact"; -import { CSSProperties } from "preact/compat"; +import { CSSProperties, useSyncExternalStore } from "preact/compat"; import { MutableRef, useCallback, useContext, useDebugValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks"; import appContext, { EventData, EventNames } from "../../components/app_context"; @@ -25,6 +25,7 @@ import utils, { escapeRegExp, randomString, reloadFrontendApp } from "../../serv import BasicWidget, { ReactWrappedWidget } from "../basic_widget"; import NoteContextAwareWidget from "../note_context_aware_widget"; import { DragData } from "../note_tree"; +import { noteSavedDataStore } from "./NoteStore"; import { NoteContextContext, ParentComponent, refToJQuerySelector } from "./react_utils"; export function useTriliumEvent(eventName: T, handler: (data: EventData) => void) { @@ -112,6 +113,7 @@ export function useEditorSpacedUpdate({ note, noteContext, getData, onContentCha protected_session_holder.touchProtectedSessionIfNecessary(note); await server.put(`notes/${note.noteId}/data`, data, parentComponent?.componentId); + noteSavedDataStore.set(note.noteId, data.content); dataSaved?.(data); }; }, [ note, getData, dataSaved ]); @@ -120,6 +122,7 @@ export function useEditorSpacedUpdate({ note, noteContext, getData, onContentCha // React to note/blob changes. useEffect(() => { if (!blob) return; + noteSavedDataStore.set(note.noteId, blob.content); spacedUpdate.allowUpdateWithoutChange(() => onContentChange(blob.content)); }, [ blob ]); @@ -151,6 +154,14 @@ export function useEditorSpacedUpdate({ note, noteContext, getData, onContentCha return spacedUpdate; } +export function useNoteSavedData(noteId: string | undefined) { + return useSyncExternalStore( + (cb) => noteId ? noteSavedDataStore.subscribe(noteId, cb) : () => {}, + () => noteId ? noteSavedDataStore.get(noteId) : undefined + ); +} + + /** * Allows a React component to read and write a Trilium option, while also watching for external changes. * diff --git a/apps/client/src/widgets/sidebar/RightPanelContainer.tsx b/apps/client/src/widgets/sidebar/RightPanelContainer.tsx index 34ec8360c..7b838c518 100644 --- a/apps/client/src/widgets/sidebar/RightPanelContainer.tsx +++ b/apps/client/src/widgets/sidebar/RightPanelContainer.tsx @@ -6,11 +6,13 @@ import { useEffect } from "preact/hooks"; import options from "../../services/options"; import { DEFAULT_GUTTER_SIZE } from "../../services/resizer"; +import { useActiveNoteContext } from "../react/hooks"; import TableOfContents from "./TableOfContents"; const MIN_WIDTH_PERCENT = 5; export default function RightPanelContainer() { + const { note } = useActiveNoteContext(); useEffect(() => { // We are intentionally omitting useTriliumOption to avoid re-render due to size change. const rightPaneWidth = Math.max(MIN_WIDTH_PERCENT, options.getInt("rightPaneWidth") ?? MIN_WIDTH_PERCENT); @@ -26,7 +28,9 @@ export default function RightPanelContainer() { return (
- + {note && <> + + }
); } diff --git a/apps/client/src/widgets/sidebar/TableOfContents.tsx b/apps/client/src/widgets/sidebar/TableOfContents.tsx index 23e08506d..01135d440 100644 --- a/apps/client/src/widgets/sidebar/TableOfContents.tsx +++ b/apps/client/src/widgets/sidebar/TableOfContents.tsx @@ -1,11 +1,14 @@ +import FNote from "../../entities/fnote"; import { t } from "../../services/i18n"; +import { useNoteSavedData } from "../react/hooks"; import RightPanelWidget from "./RightPanelWidget"; -export default function TableOfContents() { +export default function TableOfContents({ note }: { note: FNote }) { + const content = useNoteSavedData(note.noteId); return ( - Toc is here. + {content?.length} );