Skip to content

Commit 2de2315

Browse files
committed
feat(presence): configurable idle timeout setting
1 parent 7a50b64 commit 2de2315

4 files changed

Lines changed: 83 additions & 8 deletions

File tree

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

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,7 @@ function Editor({ isMobile }: Readonly<{ isMobile: boolean }>) {
483483
<SettingTile
484484
title="Auto-Idle"
485485
focusId="auto-idle-presence"
486-
description="Automatically appear unavailable after 5 minutes of inactivity or when the app isn't active."
486+
description="Automatically appear unavailable after a period of inactivity or when the app isn't active."
487487
after={
488488
<Switch
489489
variant="Primary"
@@ -494,6 +494,16 @@ function Editor({ isMobile }: Readonly<{ isMobile: boolean }>) {
494494
/>
495495
</SequenceCard>
496496
)}
497+
{sendPresence && autoIdlePresence && (
498+
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
499+
<SettingTile
500+
title="Idle Timeout"
501+
focusId="presence-idle-timeout"
502+
description="Minutes of inactivity before appearing unavailable."
503+
after={<PresenceIdleTimeoutInput />}
504+
/>
505+
</SequenceCard>
506+
)}
497507
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
498508
<SettingTile
499509
title="Room Message Preview"
@@ -872,6 +882,52 @@ function EmojiSelectorThresholdInput() {
872882
);
873883
}
874884

885+
function PresenceIdleTimeoutInput() {
886+
const [idleTimeoutMins, setIdleTimeoutMins] = useSetting(settingsAtom, 'presenceIdleTimeoutMins');
887+
const [inputValue, setInputValue] = useState(idleTimeoutMins.toString());
888+
889+
const handleChange: ChangeEventHandler<HTMLInputElement> = (evt) => {
890+
const val = evt.target.value;
891+
setInputValue(val);
892+
const parsed = Number.parseInt(val, 10);
893+
if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 60) {
894+
setIdleTimeoutMins(parsed);
895+
}
896+
};
897+
898+
const handleKeyDown: KeyboardEventHandler<HTMLInputElement> = (evt) => {
899+
if (isKeyHotkey('escape', evt)) {
900+
evt.stopPropagation();
901+
setInputValue(idleTimeoutMins.toString());
902+
(evt.target as HTMLInputElement).blur();
903+
}
904+
if (isKeyHotkey('enter', evt)) {
905+
(evt.target as HTMLInputElement).blur();
906+
}
907+
};
908+
909+
return (
910+
<Box alignItems="Center" gap="200">
911+
<Input
912+
style={{ width: toRem(80) }}
913+
variant={Number.parseInt(inputValue, 10) === idleTimeoutMins ? 'Secondary' : 'Success'}
914+
size="300"
915+
radii="300"
916+
type="number"
917+
min="1"
918+
max="60"
919+
value={inputValue}
920+
onChange={handleChange}
921+
onKeyDown={handleKeyDown}
922+
outlined
923+
/>
924+
<Text size="T200" priority="300">
925+
min
926+
</Text>
927+
</Box>
928+
);
929+
}
930+
875931
function Calls() {
876932
const [alwaysShowCallButton, setAlwaysShowCallButton] = useSetting(
877933
settingsAtom,

src/app/hooks/useUserPresence.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useEffect, useMemo, useState } from 'react';
2-
import type { User, UserEventHandlerMap } from '$types/matrix-sdk';
3-
import { UserEvent } from '$types/matrix-sdk';
2+
import type { MatrixEvent, User, UserEventHandlerMap } from '$types/matrix-sdk';
3+
import { ClientEvent, UserEvent } from '$types/matrix-sdk';
44
import { useMatrixClient } from './useMatrixClient';
55

66
export enum Presence {
@@ -31,7 +31,21 @@ export const useUserPresence = (userId: string): UserPresence | undefined => {
3131
useEffect(() => {
3232
if (!user) {
3333
setPresence(undefined);
34-
return undefined;
34+
35+
// When the user isn't in the SDK store yet (e.g., presence arrived before
36+
// any membership event), listen on the client for incoming events so we
37+
// can re-evaluate once a presence event for this user is stored.
38+
const handleEvent = (event: MatrixEvent) => {
39+
if (event.getType() !== 'm.presence') return;
40+
const sender = event.getSender();
41+
if (sender !== userId) return;
42+
const latestUser = mx.getUser(userId);
43+
if (latestUser) setPresence(getUserPresence(latestUser));
44+
};
45+
mx.on(ClientEvent.Event, handleEvent);
46+
return () => {
47+
mx.removeListener(ClientEvent.Event, handleEvent);
48+
};
3549
}
3650
setPresence(getUserPresence(user));
3751
const updatePresence: UserEventHandlerMap[UserEvent.Presence] = (e, u) => {
@@ -48,7 +62,7 @@ export const useUserPresence = (userId: string): UserPresence | undefined => {
4862
user.removeListener(UserEvent.CurrentlyActive, updatePresence);
4963
user.removeListener(UserEvent.LastPresenceTs, updatePresence);
5064
};
51-
}, [user]);
65+
}, [mx, user, userId]);
5266

5367
return presence;
5468
};

src/app/pages/client/ClientNonUIFeatures.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -845,6 +845,7 @@ function PresenceFeature() {
845845
const mx = useMatrixClient();
846846
const [sendPresence] = useSetting(settingsAtom, 'sendPresence');
847847
const [autoIdlePresence] = useSetting(settingsAtom, 'autoIdlePresence');
848+
const [presenceIdleTimeoutMins] = useSetting(settingsAtom, 'presenceIdleTimeoutMins');
848849

849850
useEffect(() => {
850851
// Classic sync: set_presence query param on every /sync poll.
@@ -859,12 +860,12 @@ function PresenceFeature() {
859860
}
860861
}, [mx, sendPresence]);
861862

862-
// Auto-idle: set presence to unavailable after 5 minutes of inactivity or
863+
// Auto-idle: set presence to unavailable after inactivity or
863864
// when the tab is hidden, and restore online on activity.
864865
useEffect(() => {
865866
if (!sendPresence || !autoIdlePresence) return undefined;
866867

867-
const IDLE_TIMEOUT_MS = 5 * 60 * 1000;
868+
const IDLE_TIMEOUT_MS = Math.max(1, presenceIdleTimeoutMins) * 60 * 1000;
868869
let idleTimer: ReturnType<typeof setTimeout> | undefined;
869870
let isIdle = false;
870871

@@ -915,7 +916,7 @@ function PresenceFeature() {
915916
mx.setPresence({ presence: 'online' }).catch(() => {});
916917
}
917918
};
918-
}, [mx, sendPresence, autoIdlePresence]);
919+
}, [mx, sendPresence, autoIdlePresence, presenceIdleTimeoutMins]);
919920

920921
return null;
921922
}

src/app/state/settings.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export interface Settings {
7373
isWidgetDrawer: boolean;
7474
memberSortFilterIndex: number;
7575
enterForNewline: boolean;
76+
isMarkdown: boolean;
7677
editorToolbar: boolean;
7778
composerToolbarOpen: boolean;
7879
messageLayout: MessageLayout;
@@ -127,6 +128,7 @@ export interface Settings {
127128
// Sable features!
128129
sendPresence: boolean;
129130
autoIdlePresence: boolean;
131+
presenceIdleTimeoutMins: number;
130132
showRoomMessagePreview: boolean;
131133
mobileGestures: boolean;
132134
rightSwipeAction: RightSwipeAction;
@@ -197,6 +199,7 @@ export const defaultSettings: Settings = {
197199
isWidgetDrawer: false,
198200
memberSortFilterIndex: 0,
199201
enterForNewline: false,
202+
isMarkdown: true,
200203
editorToolbar: false,
201204
composerToolbarOpen: false,
202205
messageLayout: 0,
@@ -252,6 +255,7 @@ export const defaultSettings: Settings = {
252255
// Sable features!
253256
sendPresence: true,
254257
autoIdlePresence: true,
258+
presenceIdleTimeoutMins: 5,
255259
showRoomMessagePreview: true,
256260
mobileGestures: true,
257261
rightSwipeAction: RightSwipeAction.Reply,

0 commit comments

Comments
 (0)