fix(presence): Do Not Disturb is real "busy" presence and keeps your status (#283)
CI / Build & Quality Checks (pull_request) Successful in 1m52s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 8s
CI / Playwright smoke (e2e) (pull_request) Successful in 10m59s
CI / Build & Quality Checks (pull_request) Successful in 1m52s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 8s
CI / Playwright smoke (e2e) (pull_request) Successful in 10m59s
DND was sent as presence unavailable with the literal status text "dnd": it replaced your status, and other clients showed "dnd". Where the server supports MSC3026 (busy_presence), DND now sends presence org.matrix.msc3026.busy and leaves the status alone (Synapse keeps busy across /sync set_presence=online). Busy renders as red "Do Not Disturb" with the real status line; the legacy unavailable + "dnd" encoding is still recognised and still used on servers without it. Status saves / clears no longer downgrade busy. The SDK rejects non-spec presences, so busy goes through a direct authed PUT. Fixes #283 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
04605f2680
commit
00f962a463
@@ -1,20 +1,27 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { SetPresence } from 'matrix-js-sdk';
|
||||
import { PresenceDeduper, PresenceWrite } from '../utils/presenceWrite';
|
||||
import { PresenceDeduper, PresenceWrite, writePresence } from '../utils/presenceWrite';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
import { getLastLocalStatusWrite, pickHeartbeatStatus } from '../utils/ownStatusSync';
|
||||
import { useSetting } from '../state/hooks/settings';
|
||||
import { settingsAtom } from '../state/settings';
|
||||
import {
|
||||
BUSY_PRESENCE,
|
||||
BUSY_PRESENCE_FEATURE,
|
||||
isBusyPresenceSupported,
|
||||
setBusyPresenceSupported,
|
||||
} from '../utils/busyPresence';
|
||||
|
||||
const IDLE_TIMEOUT_MS = 10 * 60 * 1000;
|
||||
const ACTIVITY_THROTTLE_MS = 1000;
|
||||
|
||||
export type PresenceSetting = 'auto' | 'online' | 'idle' | 'dnd' | 'invisible';
|
||||
export type PresenceState = 'online' | 'unavailable' | 'offline';
|
||||
export type PresenceState = 'online' | 'unavailable' | 'offline' | typeof BUSY_PRESENCE;
|
||||
|
||||
/**
|
||||
* Single source of truth for mapping the user's presence preference to the
|
||||
* Matrix presence value: auto/online → 'online', idle/dnd → 'unavailable',
|
||||
* Matrix presence value: auto/online → 'online', idle → 'unavailable', dnd →
|
||||
* MSC3026 busy (or 'unavailable' where the server lacks it),
|
||||
* invisible (or the hidePresence override) → 'offline'. Shared with the Profile
|
||||
* status writer so setting/clearing a status message never overrides the user's
|
||||
* chosen presence (e.g. outing an Invisible user as online).
|
||||
@@ -24,7 +31,10 @@ export function presenceStateFromSetting(
|
||||
hidePresence: boolean,
|
||||
): PresenceState {
|
||||
if (hidePresence || presenceStatus === 'invisible') return 'offline';
|
||||
if (presenceStatus === 'idle' || presenceStatus === 'dnd') return 'unavailable';
|
||||
// [Gitea #283] DND is MSC3026 busy where the server supports it, so status
|
||||
// writes (save / clear / auto-clear) don't downgrade it to "unavailable".
|
||||
if (presenceStatus === 'dnd') return isBusyPresenceSupported() ? BUSY_PRESENCE : 'unavailable';
|
||||
if (presenceStatus === 'idle') return 'unavailable';
|
||||
return 'online';
|
||||
}
|
||||
|
||||
@@ -62,8 +72,7 @@ export function usePresenceUpdater() {
|
||||
// by a heartbeat that changed nothing.
|
||||
const send = (write: PresenceWrite, label: string) => {
|
||||
if (!deduper.shouldSend(write)) return Promise.resolve();
|
||||
return mx
|
||||
.setPresence(write)
|
||||
return writePresence(mx, write)
|
||||
.then(() => deduper.sent(write))
|
||||
.catch((err) => warnPresenceFailure(label, err));
|
||||
};
|
||||
@@ -134,7 +143,22 @@ export function usePresenceUpdater() {
|
||||
return undefined;
|
||||
}
|
||||
if (presenceStatus === 'dnd') {
|
||||
setUnavailable('dnd');
|
||||
// [Gitea #283] Real "busy" presence where supported: the status text
|
||||
// stays yours. /sync reporting "online" doesn't override busy on
|
||||
// Synapse, so the sync loop can stay online. Older servers keep the
|
||||
// legacy unavailable + "dnd" text encoding.
|
||||
mx.doesServerSupportUnstableFeature(BUSY_PRESENCE_FEATURE)
|
||||
.catch(() => false)
|
||||
.then(async (busy) => {
|
||||
setBusyPresenceSupported(busy);
|
||||
if (!busy) {
|
||||
setUnavailable('dnd');
|
||||
return;
|
||||
}
|
||||
syncPresence(SetPresence.Online);
|
||||
const status = await resolveStatus();
|
||||
send({ presence: BUSY_PRESENCE, ...(status ? { status_msg: status } : {}) }, 'busy');
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user