From 21cf5e1df725d34dda22322c8a7d8e9b3ff243cd Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 3 Jan 2026 20:29:54 +0200 Subject: [PATCH] chore(client/pdf): use custom spaced update hook --- apps/client/src/widgets/react/hooks.tsx | 65 +++++++++++++++++++ .../src/widgets/type_widgets/file/Pdf.tsx | 29 ++++++--- 2 files changed, 84 insertions(+), 10 deletions(-) diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index 7fade29b97..1063def55b 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -170,6 +170,71 @@ export function useEditorSpacedUpdate({ note, noteType, noteContext, getData, on return spacedUpdate; } +export function useBlobEditorSpacedUpdate({ note, noteType, noteContext, getData, onContentChange, dataSaved, updateInterval }: { + noteType: NoteType; + note: FNote, + noteContext: NoteContext | null | undefined, + getData: () => Promise | Blob | undefined, + onContentChange: (newBlob: FBlob) => void, + dataSaved?: (savedData: Blob) => void, + updateInterval?: number; +}) { + const parentComponent = useContext(ParentComponent); + const blob = useNoteBlob(note, parentComponent?.componentId); + + const callback = useMemo(() => { + return async () => { + const data = await getData(); + + // for read only notes + if (data === undefined || note.type !== noteType) return; + + protected_session_holder.touchProtectedSessionIfNecessary(note); + await server.upload(`notes/${note.noteId}/file`, new File([ data ], note.title, { type: note.mime }), parentComponent?.componentId); + dataSaved?.(data); + }; + }, [ note, getData, dataSaved, noteType, parentComponent ]); + const stateCallback = useCallback((state) => { + noteContext?.setContextData("saveState", { + state + }); + }, [ noteContext ]); + const spacedUpdate = useSpacedUpdate(callback, updateInterval, stateCallback); + + // React to note/blob changes. + useEffect(() => { + if (!blob) return; + spacedUpdate.allowUpdateWithoutChange(() => onContentChange(blob)); + }, [ blob ]); + + // React to update interval changes. + useEffect(() => { + if (!updateInterval) return; + spacedUpdate.setUpdateInterval(updateInterval); + }, [ updateInterval ]); + + // Save if needed upon switching tabs. + useTriliumEvent("beforeNoteSwitch", async ({ noteContext: eventNoteContext }) => { + if (eventNoteContext.ntxId !== noteContext?.ntxId) return; + await spacedUpdate.updateNowIfNecessary(); + }); + + // Save if needed upon tab closing. + useTriliumEvent("beforeNoteContextRemove", async ({ ntxIds }) => { + if (!noteContext?.ntxId || !ntxIds.includes(noteContext.ntxId)) return; + await spacedUpdate.updateNowIfNecessary(); + }); + + // Save if needed upon window/browser closing. + useEffect(() => { + const listener = () => spacedUpdate.isAllSavedAndTriggerUpdate(); + appContext.addBeforeUnloadListener(listener); + return () => appContext.removeBeforeUnloadListener(listener); + }, []); + + return spacedUpdate; +} + export function useNoteSavedData(noteId: string | undefined) { return useSyncExternalStore( (cb) => noteId ? noteSavedDataStore.subscribe(noteId, cb) : () => {}, diff --git a/apps/client/src/widgets/type_widgets/file/Pdf.tsx b/apps/client/src/widgets/type_widgets/file/Pdf.tsx index ad4bb8f569..cbd34eef8d 100644 --- a/apps/client/src/widgets/type_widgets/file/Pdf.tsx +++ b/apps/client/src/widgets/type_widgets/file/Pdf.tsx @@ -4,9 +4,8 @@ import appContext from "../../../components/app_context"; import type NoteContext from "../../../components/note_context"; import FBlob from "../../../entities/fblob"; import FNote from "../../../entities/fnote"; -import server from "../../../services/server"; import { useViewModeConfig } from "../../collections/NoteList"; -import { useTriliumEvent } from "../../react/hooks"; +import { useBlobEditorSpacedUpdate, useTriliumEvent } from "../../react/hooks"; import PdfViewer from "./PdfViewer"; export default function PdfPreview({ note, blob, componentId, noteContext }: { @@ -17,13 +16,30 @@ export default function PdfPreview({ note, blob, componentId, noteContext }: { }) { const iframeRef = useRef(null); const historyConfig = useViewModeConfig(note, "pdfHistory"); + const dataRef = useRef(new Blob()); + + const spacedUpdate = useBlobEditorSpacedUpdate({ + note, + noteType: "file", + noteContext, + getData() { + return dataRef.current; + }, + onContentChange() { + if (iframeRef.current?.contentWindow) { + iframeRef.current.contentWindow.location.reload(); + } + } + }); useEffect(() => { function handleMessage(event: PdfMessageEvent) { if (event.data?.type === "pdfjs-viewer-document-modified") { const blob = new Blob([event.data.data as Uint8Array], { type: note.mime }); if (event.data.noteId === note.noteId && event.data.ntxId === noteContext.ntxId) { - server.upload(`notes/${note.noteId}/file`, new File([blob], note.title, { type: note.mime }), componentId); + dataRef.current = blob; + spacedUpdate.resetUpdateTimer(); + spacedUpdate.scheduleUpdate(); } } @@ -138,13 +154,6 @@ export default function PdfPreview({ note, blob, componentId, noteContext }: { }; }, [ note, historyConfig, componentId, blob, noteContext ]); - // Refresh when blob changes. - useEffect(() => { - if (iframeRef.current?.contentWindow) { - iframeRef.current.contentWindow.location.reload(); - } - }, [ blob ]); - useTriliumEvent("customDownload", ({ ntxId }) => { if (ntxId !== noteContext.ntxId) return; iframeRef.current?.contentWindow?.postMessage({