Skip to content

Commit 08abe08

Browse files
authored
Merge pull request #108 from Just-Insane/feat/presence-toggle
feat(presence): presence status setting
2 parents 2df0e42 + 5b1521d commit 08abe08

5 files changed

Lines changed: 53 additions & 4 deletions

File tree

.changeset/feat-presence-toggle.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
sable: minor
3+
---
4+
5+
Adds a **Presence Status** toggle under Settings → General.
6+
7+
- New `sendPresence` setting (boolean, default `true`) persisted in localStorage
8+
- When disabled, the MSC4186 presence extension sends `{ enabled: false }` so the server stops delivering presence events
9+
- Also disables presence for classic sync via `client.setSyncPresence('offline')`
10+
- Takes effect at runtime — no reconnect needed

src/app/features/settings/general/General.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,7 @@ function Editor({ isMobile }: { isMobile: boolean }) {
410410
const [isMarkdown, setIsMarkdown] = useSetting(settingsAtom, 'isMarkdown');
411411
const [hideActivity, setHideActivity] = useSetting(settingsAtom, 'hideActivity');
412412
const [hideReads, setHideReads] = useSetting(settingsAtom, 'hideReads');
413+
const [sendPresence, setSendPresence] = useSetting(settingsAtom, 'sendPresence');
413414

414415
return (
415416
<Box direction="Column" gap="100">
@@ -453,6 +454,13 @@ function Editor({ isMobile }: { isMobile: boolean }) {
453454
after={<Switch variant="Primary" value={hideReads} onChange={setHideReads} />}
454455
/>
455456
</SequenceCard>
457+
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
458+
<SettingTile
459+
title="Presence Status"
460+
description="Show and receive online status from other users."
461+
after={<Switch variant="Primary" value={sendPresence} onChange={setSendPresence} />}
462+
/>
463+
</SequenceCard>
456464
</Box>
457465
);
458466
}

src/app/pages/client/ClientNonUIFeatures.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useAtomValue, useSetAtom } from 'jotai';
22
import { ReactNode, useCallback, useEffect, useRef } from 'react';
33
import { useNavigate } from 'react-router-dom';
4-
import { PushProcessor, RoomEvent, RoomEventHandlerMap } from '$types/matrix-sdk';
4+
import { PushProcessor, RoomEvent, RoomEventHandlerMap, SetPresence } from '$types/matrix-sdk';
55
import parse from 'html-react-parser';
66
import { getReactCustomHtmlParser, LINKIFY_OPTS } from '$plugins/react-custom-html-parser';
77
import { sanitizeCustomHtml } from '$utils/sanitize';
@@ -38,6 +38,7 @@ import {
3838
} from '$utils/notificationStyle';
3939
import { mobileOrTablet } from '$utils/user-agent';
4040
import { useSlidingSyncActiveRoom } from '$hooks/useSlidingSyncActiveRoom';
41+
import { getSlidingSyncManager } from '$client/initMatrix';
4142
import { getInboxInvitesPath } from '../pathUtils';
4243
import { BackgroundNotifications } from './BackgroundNotifications';
4344

@@ -575,6 +576,21 @@ function SlidingSyncActiveRoomSubscriber() {
575576
return null;
576577
}
577578

579+
function PresenceFeature() {
580+
const mx = useMatrixClient();
581+
const [sendPresence] = useSetting(settingsAtom, 'sendPresence');
582+
583+
useEffect(() => {
584+
// Classic sync: set_presence query param on every /sync poll.
585+
// Passing undefined restores the default (online); Offline suppresses broadcasting.
586+
mx.setSyncPresence(sendPresence ? undefined : SetPresence.Offline);
587+
// Sliding sync: enable/disable the presence extension on the next poll.
588+
getSlidingSyncManager(mx)?.setPresenceEnabled(sendPresence);
589+
}, [mx, sendPresence]);
590+
591+
return null;
592+
}
593+
578594
export function ClientNonUIFeatures({ children }: ClientNonUIFeaturesProps) {
579595
return (
580596
<>
@@ -587,6 +603,7 @@ export function ClientNonUIFeatures({ children }: ClientNonUIFeaturesProps) {
587603
<BackgroundNotifications />
588604
<SyncNotificationSettingsWithServiceWorker />
589605
<SlidingSyncActiveRoomSubscriber />
606+
<PresenceFeature />
590607
{children}
591608
</>
592609
);

src/app/state/settings.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export interface Settings {
7878
captionPosition: CaptionPosition;
7979

8080
// Sable features!
81+
sendPresence: boolean;
8182
mobileGestures: boolean;
8283
rightSwipeAction: RightSwipeAction;
8384
hideMembershipInReadOnly: boolean;
@@ -153,6 +154,7 @@ const defaultSettings: Settings = {
153154
captionPosition: CaptionPosition.Below,
154155

155156
// Sable features!
157+
sendPresence: true,
156158
mobileGestures: true,
157159
rightSwipeAction: RightSwipeAction.Reply,
158160
hideMembershipInReadOnly: true,

src/client/slidingSync.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,14 @@ const getListEndIndex = (list: MSC3575List | null): number => {
242242
// poll and feeds received `m.presence` events into the SDK's User objects so that
243243
// components using `useUserPresence` see live updates (same path as regular /sync).
244244
class ExtensionPresence implements Extension<{ enabled: boolean }, { events?: object[] }> {
245+
private enabled = true;
246+
245247
public constructor(private readonly mx: MatrixClient) {}
246248

249+
public setEnabled(value: boolean): void {
250+
this.enabled = value;
251+
}
252+
247253
// eslint-disable-next-line class-methods-use-this
248254
public name(): string {
249255
return 'presence';
@@ -255,9 +261,8 @@ class ExtensionPresence implements Extension<{ enabled: boolean }, { events?: ob
255261
return ExtensionState.PostProcess;
256262
}
257263

258-
// eslint-disable-next-line class-methods-use-this
259264
public async onRequest(): Promise<{ enabled: boolean }> {
260-
return { enabled: true };
265+
return { enabled: this.enabled };
261266
}
262267

263268
public async onResponse(data: { events?: object[] }): Promise<void> {
@@ -301,6 +306,8 @@ export class SlidingSyncManager {
301306

302307
private readonly onLifecycle: (state: SlidingSyncState, resp: unknown, err?: Error) => void;
303308

309+
private presenceExtension!: ExtensionPresence;
310+
304311
public readonly slidingSync: SlidingSync;
305312

306313
public readonly probeTimeoutMs: number;
@@ -333,7 +340,8 @@ export class SlidingSyncManager {
333340

334341
// Register the presence extension so m.presence events from the server are fed
335342
// into the SDK's User objects, keeping useUserPresence accurate during sliding sync.
336-
this.slidingSync.registerExtension(new ExtensionPresence(mx));
343+
this.presenceExtension = new ExtensionPresence(mx);
344+
this.slidingSync.registerExtension(this.presenceExtension);
337345

338346
// Register a custom subscription for unencrypted active rooms; encrypted rooms use
339347
// the default subscription (which already has [*,*]).
@@ -410,6 +418,10 @@ export class SlidingSyncManager {
410418
);
411419
}
412420

421+
public setPresenceEnabled(enabled: boolean): void {
422+
this.presenceExtension.setEnabled(enabled);
423+
}
424+
413425
public getDiagnostics(): SlidingSyncDiagnostics {
414426
return {
415427
proxyBaseUrl: this.proxyBaseUrl,

0 commit comments

Comments
 (0)