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;