From 6bd2903de1b1de919f7d0e5c8d0cce007c06459c Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Sat, 12 Sep 2026 14:48:35 -0400 Subject: [PATCH] fix(profile): private notes no longer lose typed text The store-sync effect re-applied the stored note on every notification, including the echo of the note's own save, reverting text typed after the debounce fired; and closing the panel inside the 800ms debounce dropped the pending save. Add a dirty ref that suppresses the resync while there are unsaved edits, and flush the pending save on unmount. Fixes #18 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA --- .../user-profile/UserRoomProfile.tsx | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/src/app/components/user-profile/UserRoomProfile.tsx b/src/app/components/user-profile/UserRoomProfile.tsx index 1b2c25881..fc8eb8c29 100644 --- a/src/app/components/user-profile/UserRoomProfile.tsx +++ b/src/app/components/user-profile/UserRoomProfile.tsx @@ -214,24 +214,56 @@ function UserPrivateNotes({ userId }: { userId: string }) { const [draft, setDraft] = useState(() => getNote(userId)); const [saving, setSaving] = useState(false); const saveTimer = useRef | undefined>(undefined); + // True while the user has unsaved local edits — prevents the store-sync + // effect below from reacting to the echo of our own save and reverting text + // typed after the debounce fired but before that save's account-data echo + // landed (mirrors statusDirtyRef in Profile.tsx's ProfileStatus). + const dirtyRef = useRef(false); + // Latest draft/userId, kept current on every render so the unmount cleanup + // can flush a pending save without capturing a stale closure. + const draftRef = useRef(draft); + draftRef.current = draft; + const userIdRef = useRef(userId); + userIdRef.current = userId; + const setNoteRef = useRef(setNote); + setNoteRef.current = setNote; + const prevUserIdRef = useRef(userId); - // Sync if account data arrives after mount + // Sync if account data arrives after mount, but never while there are + // unsaved local edits (including our own save's in-flight echo). useEffect(() => { + if (prevUserIdRef.current !== userId) { + prevUserIdRef.current = userId; + dirtyRef.current = false; + } + if (dirtyRef.current) return; setDraft(getNote(userId)); }, [getNote, userId]); const handleChange = (e: React.ChangeEvent) => { const val = e.target.value; + dirtyRef.current = true; setDraft(val); clearTimeout(saveTimer.current); saveTimer.current = setTimeout(async () => { + dirtyRef.current = false; setSaving(true); await setNote(userId, val); setSaving(false); }, 800); }; - useEffect(() => () => clearTimeout(saveTimer.current), []); + useEffect( + () => () => { + clearTimeout(saveTimer.current); + // Flush a still-pending debounced save instead of dropping it (e.g. the + // profile panel closes within the 800ms debounce window). + if (dirtyRef.current) { + setNoteRef.current(userIdRef.current, draftRef.current); + } + }, + [], + ); const charsLeft = USER_NOTE_MAX_LENGTH - draft.length;