Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/feat-presence-toggle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
sable: minor
---

Adds a **Presence Status** toggle under Settings → General.

- New `sendPresence` setting (boolean, default `true`) persisted in localStorage
- When disabled, the MSC4186 presence extension sends `{ enabled: false }` so the server stops delivering presence events
- Also disables presence for classic sync via `client.setSyncPresence('offline')`
- Takes effect at runtime — no reconnect needed
8 changes: 8 additions & 0 deletions src/app/features/settings/general/General.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,7 @@ function Editor({ isMobile }: { isMobile: boolean }) {
const [isMarkdown, setIsMarkdown] = useSetting(settingsAtom, 'isMarkdown');
const [hideActivity, setHideActivity] = useSetting(settingsAtom, 'hideActivity');
const [hideReads, setHideReads] = useSetting(settingsAtom, 'hideReads');
const [sendPresence, setSendPresence] = useSetting(settingsAtom, 'sendPresence');

return (
<Box direction="Column" gap="100">
Expand Down Expand Up @@ -453,6 +454,13 @@ function Editor({ isMobile }: { isMobile: boolean }) {
after={<Switch variant="Primary" value={hideReads} onChange={setHideReads} />}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Presence Status"
description="Show and receive online status from other users."
after={<Switch variant="Primary" value={sendPresence} onChange={setSendPresence} />}
/>
</SequenceCard>
</Box>
);
}
Expand Down
19 changes: 18 additions & 1 deletion src/app/pages/client/ClientNonUIFeatures.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useAtomValue, useSetAtom } from 'jotai';
import { ReactNode, useCallback, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { PushProcessor, RoomEvent, RoomEventHandlerMap } from '$types/matrix-sdk';
import { PushProcessor, RoomEvent, RoomEventHandlerMap, SetPresence } from '$types/matrix-sdk';
import parse from 'html-react-parser';
import { getReactCustomHtmlParser, LINKIFY_OPTS } from '$plugins/react-custom-html-parser';
import { sanitizeCustomHtml } from '$utils/sanitize';
Expand Down Expand Up @@ -38,6 +38,7 @@ import {
} from '$utils/notificationStyle';
import { mobileOrTablet } from '$utils/user-agent';
import { useSlidingSyncActiveRoom } from '$hooks/useSlidingSyncActiveRoom';
import { getSlidingSyncManager } from '$client/initMatrix';
import { getInboxInvitesPath } from '../pathUtils';
import { BackgroundNotifications } from './BackgroundNotifications';

Expand Down Expand Up @@ -575,6 +576,21 @@ function SlidingSyncActiveRoomSubscriber() {
return null;
}

function PresenceFeature() {
const mx = useMatrixClient();
const [sendPresence] = useSetting(settingsAtom, 'sendPresence');

useEffect(() => {
// Classic sync: set_presence query param on every /sync poll.
// Passing undefined restores the default (online); Offline suppresses broadcasting.
mx.setSyncPresence(sendPresence ? undefined : SetPresence.Offline);
// Sliding sync: enable/disable the presence extension on the next poll.
getSlidingSyncManager(mx)?.setPresenceEnabled(sendPresence);
}, [mx, sendPresence]);

return null;
}

export function ClientNonUIFeatures({ children }: ClientNonUIFeaturesProps) {
return (
<>
Expand All @@ -587,6 +603,7 @@ export function ClientNonUIFeatures({ children }: ClientNonUIFeaturesProps) {
<BackgroundNotifications />
<SyncNotificationSettingsWithServiceWorker />
<SlidingSyncActiveRoomSubscriber />
<PresenceFeature />
{children}
</>
);
Expand Down
2 changes: 2 additions & 0 deletions src/app/state/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export interface Settings {
captionPosition: CaptionPosition;

// Sable features!
sendPresence: boolean;
mobileGestures: boolean;
rightSwipeAction: RightSwipeAction;
hideMembershipInReadOnly: boolean;
Expand Down Expand Up @@ -153,6 +154,7 @@ const defaultSettings: Settings = {
captionPosition: CaptionPosition.Below,

// Sable features!
sendPresence: true,
mobileGestures: true,
rightSwipeAction: RightSwipeAction.Reply,
hideMembershipInReadOnly: true,
Expand Down
18 changes: 15 additions & 3 deletions src/client/slidingSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,14 @@ const getListEndIndex = (list: MSC3575List | null): number => {
// poll and feeds received `m.presence` events into the SDK's User objects so that
// components using `useUserPresence` see live updates (same path as regular /sync).
class ExtensionPresence implements Extension<{ enabled: boolean }, { events?: object[] }> {
private enabled = true;

public constructor(private readonly mx: MatrixClient) {}

public setEnabled(value: boolean): void {
this.enabled = value;
}

// eslint-disable-next-line class-methods-use-this
public name(): string {
return 'presence';
Expand All @@ -255,9 +261,8 @@ class ExtensionPresence implements Extension<{ enabled: boolean }, { events?: ob
return ExtensionState.PostProcess;
}

// eslint-disable-next-line class-methods-use-this
public async onRequest(): Promise<{ enabled: boolean }> {
return { enabled: true };
return { enabled: this.enabled };
}

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

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

private presenceExtension!: ExtensionPresence;

public readonly slidingSync: SlidingSync;

public readonly probeTimeoutMs: number;
Expand Down Expand Up @@ -333,7 +340,8 @@ export class SlidingSyncManager {

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

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

public setPresenceEnabled(enabled: boolean): void {
this.presenceExtension.setEnabled(enabled);
}

public getDiagnostics(): SlidingSyncDiagnostics {
return {
proxyBaseUrl: this.proxyBaseUrl,
Expand Down
Loading