diff --git a/src/app/pages/client/ClientNonUIFeatures.tsx b/src/app/pages/client/ClientNonUIFeatures.tsx index 7d3e64ca1..ff1672824 100644 --- a/src/app/pages/client/ClientNonUIFeatures.tsx +++ b/src/app/pages/client/ClientNonUIFeatures.tsx @@ -60,6 +60,7 @@ import { unmuteRoom, } from '../../features/room-nav/muteTimers'; import { STATUS_EXPIRY_KEY, STATUS_MSG_KEY } from '../../features/settings/account/Profile'; +import { setPresenceWithRetry } from '../../utils/presenceWrite'; import { useDeepLinkNavigate } from '../../hooks/useDeepLinkNavigate'; import { toastQueueAtom } from '../../state/toast'; import { useReminders } from '../../hooks/useReminders'; @@ -389,21 +390,38 @@ function StatusExpiryMonitor() { const expiryKey = STATUS_EXPIRY_KEY(userId); const msgKey = STATUS_MSG_KEY(userId); + // [Gitea #227] The clear must survive a failed write. Synapse rate-limits + // presence to ~1 write / 10 s per user (#226) and the heartbeat spends that + // budget, so a bare setPresence here 429s often — and because the keys used + // to be removed BEFORE the request, a single failure dropped the auto-clear + // permanently and the status stayed set forever. Retry the write (honouring + // Retry-After), and only forget the status once the server has taken it. + let clearing = false; const check = () => { + if (clearing) return; const stored = localStorage.getItem(expiryKey); if (!stored) return; const ts = parseInt(stored, 10); if (!ts || Date.now() < ts) return; - localStorage.removeItem(msgKey); - localStorage.removeItem(expiryKey); - mx.setPresence({ + clearing = true; + setPresenceWithRetry(mx, { presence: presenceStateFromSetting(presenceStatusRef.current, hidePresenceRef.current), status_msg: '', - }).catch(() => undefined); + }) + .then(() => { + localStorage.removeItem(msgKey); + localStorage.removeItem(expiryKey); + }) + .catch(() => undefined) + .finally(() => { + clearing = false; + }); }; check(); - const interval = setInterval(check, 30_000); + // Poll often enough that a rate-limited retry lands promptly, but not so + // often that a persistent failure hammers the server. + const interval = setInterval(check, 15_000); const onVisible = () => { if (document.visibilityState === 'visible') check(); };