Commit 9ca71229 authored by boojack's avatar boojack

fix: preserve draft content when tab is suspended or editor remounts

Gate auto-save on initialization to prevent empty content from overwriting
cached drafts on remount. Flush drafts synchronously on visibilitychange/
pagehide so Chromium tab suspension cannot drop pending debounced saves.
Restore cached draft over saved memo content when they differ (inline editing).
parent 938c405b
import { useEffect } from "react";
import { useEffect, useRef } from "react";
import { cacheService } from "../services";
export const useAutoSave = (content: string, username: string, cacheKey: string | undefined) => {
export const useAutoSave = (content: string, username: string, cacheKey: string | undefined, enabled = true) => {
const latestContentRef = useRef(content);
useEffect(() => {
latestContentRef.current = content;
}, [content]);
useEffect(() => {
if (!enabled) return;
const key = cacheService.key(username, cacheKey);
cacheService.save(key, content);
}, [content, username, cacheKey]);
}, [content, username, cacheKey, enabled]);
useEffect(() => {
if (!enabled) return;
const key = cacheService.key(username, cacheKey);
const flushDraft = () => {
cacheService.saveNow(key, latestContentRef.current);
};
const handleVisibilityChange = () => {
if (document.visibilityState === "hidden") {
flushDraft();
}
};
window.addEventListener("pagehide", flushDraft);
document.addEventListener("visibilitychange", handleVisibilityChange);
return () => {
// Flush on unmount (e.g. editor closes) to ensure the draft is persisted
// before the component is torn down — distinct from the visibility flush above.
flushDraft();
window.removeEventListener("pagehide", flushDraft);
document.removeEventListener("visibilitychange", handleVisibilityChange);
};
}, [username, cacheKey, enabled]);
};
import { useEffect, useRef } from "react";
import { useEffect, useRef, useState } from "react";
import type { Memo, Visibility } from "@/types/proto/api/v1/memo_service_pb";
import type { EditorRefActions } from "../Editor";
import { cacheService, memoService } from "../services";
......@@ -16,15 +16,25 @@ interface UseMemoInitOptions {
export const useMemoInit = ({ editorRef, memo, cacheKey, username, autoFocus, defaultVisibility }: UseMemoInitOptions) => {
const { actions, dispatch } = useEditorContext();
const initializedRef = useRef(false);
const [isInitialized, setIsInitialized] = useState(false);
useEffect(() => {
if (initializedRef.current) return;
initializedRef.current = true;
const key = cacheService.key(username, cacheKey);
const cachedContent = cacheService.load(key);
if (memo) {
dispatch(actions.initMemo(memoService.fromMemo(memo)));
const initialState = memoService.fromMemo(memo);
// Prefer cached draft over the saved memo content when the user had unsaved
// changes (e.g. tab was suspended mid-edit). Uses strict string comparison
// against memo.content — both values come from the same proto serialization
// path, so format is consistent and whitespace differences are intentional.
if (cachedContent.trim() && cachedContent !== memo.content) {
initialState.content = cachedContent;
}
dispatch(actions.initMemo(initialState));
} else {
const cachedContent = cacheService.load(cacheService.key(username, cacheKey));
if (cachedContent) {
dispatch(actions.updateContent(cachedContent));
}
......@@ -36,5 +46,9 @@ export const useMemoInit = ({ editorRef, memo, cacheKey, username, autoFocus, de
if (autoFocus) {
setTimeout(() => editorRef.current?.focus(), 100);
}
setIsInitialized(true);
}, [memo, cacheKey, username, autoFocus, defaultVisibility, actions, dispatch, editorRef]);
return { isInitialized };
};
......@@ -54,10 +54,10 @@ const MemoEditorImpl: React.FC<MemoEditorProps> = ({
// Get default visibility from user settings
const defaultVisibility = userGeneralSetting?.memoVisibility ? convertVisibilityFromString(userGeneralSetting.memoVisibility) : undefined;
useMemoInit({ editorRef, memo, cacheKey, username: currentUser?.name ?? "", autoFocus, defaultVisibility });
const { isInitialized } = useMemoInit({ editorRef, memo, cacheKey, username: currentUser?.name ?? "", autoFocus, defaultVisibility });
// Auto-save content to localStorage
useAutoSave(state.content, currentUser?.name ?? "", cacheKey);
useAutoSave(state.content, currentUser?.name ?? "", cacheKey, isInitialized);
// Focus mode management with body scroll lock
useFocusMode(state.ui.isFocusMode);
......
......@@ -26,6 +26,20 @@ export const cacheService = {
pendingSaves.set(key, timeoutId);
},
saveNow: (key: string, content: string) => {
const pendingSave = pendingSaves.get(key);
if (pendingSave) {
window.clearTimeout(pendingSave);
pendingSaves.delete(key);
}
if (content.trim()) {
localStorage.setItem(key, content);
} else {
localStorage.removeItem(key);
}
},
load(key: string): string {
return localStorage.getItem(key) || "";
},
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment