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
@@ -0,0 +1,11 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { BUSY_PRESENCE, isDndPresence } from './busyPresence';
|
||||
|
||||
test('isDndPresence: busy, or the legacy unavailable + "dnd" text (#283)', () => {
|
||||
assert.equal(isDndPresence(BUSY_PRESENCE, '🎮 Gaming'), true);
|
||||
assert.equal(isDndPresence(BUSY_PRESENCE, undefined), true);
|
||||
assert.equal(isDndPresence('unavailable', 'dnd'), true);
|
||||
assert.equal(isDndPresence('unavailable', 'lunch'), false);
|
||||
assert.equal(isDndPresence('online', 'dnd'), false);
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* [Gitea #283] Do Not Disturb as a real presence state: MSC3026 "busy"
|
||||
* (Synapse `experimental_features.msc3026_enabled`). Before, DND was sent as
|
||||
* presence `unavailable` with the literal status text "dnd", which replaced
|
||||
* your status and showed as "dnd" in other clients.
|
||||
*/
|
||||
export const BUSY_PRESENCE = 'org.matrix.msc3026.busy';
|
||||
export const BUSY_PRESENCE_FEATURE = 'org.matrix.msc3026.busy_presence';
|
||||
|
||||
let busySupported = false;
|
||||
|
||||
/** Recorded by the presence updater from the server's unstable_features. */
|
||||
export const setBusyPresenceSupported = (supported: boolean): void => {
|
||||
busySupported = supported;
|
||||
};
|
||||
|
||||
export const isBusyPresenceSupported = (): boolean => busySupported;
|
||||
|
||||
/** Pure: whether a presence means Do Not Disturb (busy, or the legacy "dnd" text). */
|
||||
export const isDndPresence = (presence: string | undefined, status: string | undefined): boolean =>
|
||||
presence === BUSY_PRESENCE || (presence === 'unavailable' && status === 'dnd');
|
||||
@@ -1,4 +1,5 @@
|
||||
import { MatrixClient, MatrixError } from 'matrix-js-sdk';
|
||||
import { MatrixClient, MatrixError, IPresenceOpts, Method } from 'matrix-js-sdk';
|
||||
import { BUSY_PRESENCE } from './busyPresence';
|
||||
|
||||
/**
|
||||
* Synapse rate-limits `PUT /presence/{user}/status` to ONE request per 10 s per
|
||||
@@ -10,7 +11,29 @@ import { MatrixClient, MatrixError } from 'matrix-js-sdk';
|
||||
* and `PresenceDeduper` lets the heartbeat skip writes that would not change
|
||||
* anything, so it stops spending the budget in the first place.
|
||||
*/
|
||||
export type PresenceWrite = { presence: 'online' | 'offline' | 'unavailable'; status_msg?: string };
|
||||
export type PresenceWrite = {
|
||||
// [Gitea #283] or MSC3026 busy (Do Not Disturb)
|
||||
presence: 'online' | 'offline' | 'unavailable' | typeof BUSY_PRESENCE;
|
||||
status_msg?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* `mx.setPresence` for a PresenceWrite. The SDK refuses anything but the three
|
||||
* spec presences ("Bad presence value"), so MSC3026 busy (#283) goes straight
|
||||
* to the same endpoint.
|
||||
*/
|
||||
export const writePresence = async (
|
||||
mx: Pick<MatrixClient, 'setPresence'> & Partial<Pick<MatrixClient, 'getSafeUserId' | 'http'>>,
|
||||
write: PresenceWrite,
|
||||
): Promise<void> => {
|
||||
if (write.presence !== BUSY_PRESENCE) {
|
||||
await mx.setPresence(write as IPresenceOpts);
|
||||
return;
|
||||
}
|
||||
if (!mx.getSafeUserId || !mx.http) throw new Error('busy presence needs a full MatrixClient');
|
||||
const path = `/presence/${encodeURIComponent(mx.getSafeUserId())}/status`;
|
||||
await mx.http.authedRequest(Method.Put, path, undefined, write);
|
||||
};
|
||||
|
||||
const sleep = (ms: number) =>
|
||||
new Promise<void>((r) => {
|
||||
@@ -28,7 +51,7 @@ export async function setPresenceWithRetry(
|
||||
for (;;) {
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await mx.setPresence(write);
|
||||
await writePresence(mx, write);
|
||||
return;
|
||||
} catch (e) {
|
||||
if (!(e instanceof MatrixError) || e.httpStatus !== 429) throw e;
|
||||
|
||||
Reference in New Issue
Block a user