From a739c25f10f23d7024278e50509ce0aacf1d3bd5 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Fri, 10 Jul 2026 00:13:44 -0400 Subject: [PATCH] fix(status): stop presence heartbeats from clobbering status edits Selecting an emoji (or any unsaved edit) in the profile Status Message field could vanish because the presence-sync effect re-applied the remote status_msg on every presence heartbeat (which fire every few seconds). The dirty-edit guard alone left a window where a heartbeat carrying the previous status overwrote the just-inserted emoji, so there was no way to add emoji to a status. Track the last remote status we synced and only react when the remote value actually changes, instead of on every heartbeat. Repeated heartbeats with an unchanged status are now ignored, so an unsaved local edit is preserved regardless of the dirty flag's timing. Cross-device status changes (a genuinely new remote value) and clears still sync, and the pending-applied stale-echo guard is unaffected. Co-Authored-By: Claude Opus 4.8 --- src/app/features/settings/account/Profile.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/app/features/settings/account/Profile.tsx b/src/app/features/settings/account/Profile.tsx index 78a0deece..cf2aade5c 100644 --- a/src/app/features/settings/account/Profile.tsx +++ b/src/app/features/settings/account/Profile.tsx @@ -363,12 +363,17 @@ function ProfileStatus() { const [presenceStatus] = useSetting(settingsAtom, 'presenceStatus'); const [hidePresence] = useSetting(settingsAtom, 'hidePresence'); - const [statusMsg, setStatusMsg] = useState( - presence?.status ?? localStorage.getItem(STATUS_MSG_KEY(userId)) ?? '', - ); + const initialStatus = presence?.status ?? localStorage.getItem(STATUS_MSG_KEY(userId)) ?? ''; + const [statusMsg, setStatusMsg] = useState(initialStatus); // True while the user has unsaved local edits — prevents a server presence // echo from overwriting what the user is currently typing/inserting. const statusDirtyRef = useRef(false); + // The last remote status we synced into the input. Presence heartbeats fire + // every few seconds carrying the SAME status; the sync effect must only react + // when the remote value actually changes, otherwise a repeated heartbeat can + // overwrite an unsaved local edit (e.g. an emoji just inserted) the instant + // the dirty flag is out of sync. Seeded with the value the input started on. + const lastSyncedRemoteRef = useRef(initialStatus); // The value we most recently applied (with the apply time). A presence // heartbeat can echo the PREVIOUS status just after we save the new one; that // stale echo would otherwise revert the input. We ignore non-matching echoes @@ -390,6 +395,10 @@ function ProfileStatus() { // wipe the saved status on every invisible toggle. if (presence.presence === Presence.Offline) return; const remoteStatus = presence.status ?? ''; + // Only act on an actual remote change. Repeated heartbeats carrying the same + // status are ignored so they can never clobber an unsaved local edit. + if (remoteStatus === lastSyncedRemoteRef.current) return; + lastSyncedRemoteRef.current = remoteStatus; const pending = pendingAppliedRef.current; if (pending) { if (remoteStatus === pending.value) {