diff --git a/apps/client/src/components/note_context.ts b/apps/client/src/components/note_context.ts index 767206167..20f4bc766 100644 --- a/apps/client/src/components/note_context.ts +++ b/apps/client/src/components/note_context.ts @@ -23,6 +23,8 @@ export interface SetNoteOpts { export type GetTextEditorCallback = (editor: CKTextEditor) => void; +export type SaveState = "saved" | "saving" | "unsaved" | "error"; + export interface NoteContextDataMap { toc: HeadingContext; pdfPages: { @@ -39,6 +41,9 @@ export interface NoteContextDataMap { layers: Array<{ id: string; name: string; visible: boolean }>; toggleLayer(layerId: string, visible: boolean): void; }; + saveState: { + state: SaveState; + } } type ContextDataKey = keyof NoteContextDataMap; diff --git a/apps/client/src/services/spaced_update.ts b/apps/client/src/services/spaced_update.ts index 938fceb00..863c6844a 100644 --- a/apps/client/src/services/spaced_update.ts +++ b/apps/client/src/services/spaced_update.ts @@ -1,22 +1,29 @@ +import type { SaveState } from "../components/note_context"; + type Callback = () => Promise | void; +export type StateCallback = (state: SaveState) => void; + export default class SpacedUpdate { private updater: Callback; private lastUpdated: number; private changed: boolean; private updateInterval: number; private changeForbidden?: boolean; + private stateCallback?: StateCallback; - constructor(updater: Callback, updateInterval = 1000) { + constructor(updater: Callback, updateInterval = 1000, stateCallback?: StateCallback) { this.updater = updater; this.lastUpdated = Date.now(); this.changed = false; this.updateInterval = updateInterval; + this.stateCallback = stateCallback; } scheduleUpdate() { if (!this.changeForbidden) { this.changed = true; + this.stateCallback?.("unsaved"); setTimeout(() => this.triggerUpdate()); } } diff --git a/apps/client/src/widgets/layout/NoteBadges.tsx b/apps/client/src/widgets/layout/NoteBadges.tsx index 5dceb31cb..9d19a5b15 100644 --- a/apps/client/src/widgets/layout/NoteBadges.tsx +++ b/apps/client/src/widgets/layout/NoteBadges.tsx @@ -7,7 +7,7 @@ import { t } from "../../services/i18n"; import { goToLinkExt } from "../../services/link"; import { Badge, BadgeWithDropdown } from "../react/Badge"; import { FormDropdownDivider, FormListItem } from "../react/FormList"; -import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean } from "../react/hooks"; +import { useGetContextData, useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean } from "../react/hooks"; import { useShareState } from "../ribbon/BasicPropertiesTab"; import { useShareInfo } from "../shared_info"; @@ -110,12 +110,13 @@ function ExecuteBadge() { } function SaveStatusBadge() { - const state: "saved" | "saving" | "unsaved" | "error" = "error"; // TODO: implement save state tracking + const saveState = useGetContextData("saveState"); + if (!saveState) return; let icon: string; let title: string; let tooltip: string; - switch (state) { + switch (saveState?.state) { case "saved": icon = "bx bx-check"; title = t("breadcrumb_badges.save_status_saved"); @@ -140,7 +141,7 @@ function SaveStatusBadge() { return ( (eventNames: T[], handler: useDebugValue(() => eventNames.join(", ")); } -export function useSpacedUpdate(callback: () => void | Promise, interval = 1000) { +export function useSpacedUpdate(callback: () => void | Promise, interval = 1000, stateCallback?: StateCallback) { const callbackRef = useRef(callback); const spacedUpdateRef = useRef(new SpacedUpdate( () => callbackRef.current(), - interval + interval, + stateCallback )); // Update callback ref when it changes @@ -121,7 +122,12 @@ export function useEditorSpacedUpdate({ note, noteType, noteContext, getData, on dataSaved?.(data); }; }, [ note, getData, dataSaved, noteType, parentComponent ]); - const spacedUpdate = useSpacedUpdate(callback); + const stateCallback = useCallback((state) => { + noteContext?.setContextData("saveState", { + state + }); + }, [ noteContext ]); + const spacedUpdate = useSpacedUpdate(callback, updateInterval, stateCallback); // React to note/blob changes. useEffect(() => {