fix(presence): Invisible, Hide Online Status, Idle and DND stick (#282)
CI / Build & Quality Checks (pull_request) Successful in 3m49s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 10s
CI / Playwright smoke (e2e) (pull_request) Successful in 13m50s

usePresenceUpdater sent a one-off PUT /presence, but the SDK kept
long-polling /sync with the default set_presence=online and Synapse treats
every sync as "online", so Invisible / Hide Online Status / Idle / DND
flipped back within seconds — a privacy setting that silently didn't work.

The presence helpers now also call mx.setSyncPresence(): offline for
Invisible / Hide Online Status, unavailable for Idle / DND (and auto-mode
idle / hidden tab), online otherwise. The PUT stays for the immediate
change.

Verified on the local Synapse, another account polling every 5 s for 60 s
after a reload in each mode: invisible → offline ×12, hidePresence →
offline ×12, idle → unavailable ×12, dnd → unavailable ×12, online →
online (currently_active) ×12; /sync carries set_presence=offline /
unavailable / online accordingly (before: online* throughout).

Fixes #282

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
This commit is contained in:
Lotus CI
2026-10-12 12:35:26 -04:00
co-authored by Claude Opus 5.5
parent 1618d71e53
commit 9b2ec91dd6
+15 -1
View File
@@ -1,4 +1,5 @@
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { SetPresence } from 'matrix-js-sdk';
import { PresenceDeduper, PresenceWrite } from '../utils/presenceWrite'; import { PresenceDeduper, PresenceWrite } from '../utils/presenceWrite';
import { useMatrixClient } from './useMatrixClient'; import { useMatrixClient } from './useMatrixClient';
import { getLastLocalStatusWrite, pickHeartbeatStatus } from '../utils/ownStatusSync'; import { getLastLocalStatusWrite, pickHeartbeatStatus } from '../utils/ownStatusSync';
@@ -90,11 +91,21 @@ export function usePresenceUpdater() {
return status; return status;
}; };
// [Gitea #282] A one-off PUT /presence doesn't stick: the SDK long-polls
// /sync with set_presence=online by default and Synapse treats every sync
// as "online", so Invisible / Idle / DND flipped back within seconds. Tell
// the sync loop which presence to report too (as Element does).
const syncPresence = (presence: SetPresence) => {
mx.setSyncPresence(presence).catch(() => undefined);
};
const setOnline = async () => { const setOnline = async () => {
syncPresence(SetPresence.Online);
const status = await resolveStatus(); const status = await resolveStatus();
return send({ presence: 'online', ...(status ? { status_msg: status } : {}) }, 'online'); return send({ presence: 'online', ...(status ? { status_msg: status } : {}) }, 'online');
}; };
const setUnavailable = async (statusMsg?: string) => { const setUnavailable = async (statusMsg?: string) => {
syncPresence(SetPresence.Unavailable);
const status = await resolveStatus(); const status = await resolveStatus();
return send( return send(
{ {
@@ -104,7 +115,10 @@ export function usePresenceUpdater() {
'unavailable', 'unavailable',
); );
}; };
const setOffline = () => send({ presence: 'offline', status_msg: '' }, 'offline'); const setOffline = () => {
syncPresence(SetPresence.Offline);
return send({ presence: 'offline', status_msg: '' }, 'offline');
};
// Manual presence overrides — no activity tracking needed. // Manual presence overrides — no activity tracking needed.
if (hidePresence || presenceStatus === 'invisible') { if (hidePresence || presenceStatus === 'invisible') {