feat(presence): presence badges, auto-idle, and Discord-style status picker#689
Closed
Just-Insane wants to merge 18 commits into
Closed
feat(presence): presence badges, auto-idle, and Discord-style status picker#689Just-Insane wants to merge 18 commits into
Just-Insane wants to merge 18 commits into
Conversation
This was referenced Apr 15, 2026
Contributor
There was a problem hiding this comment.
Pull request overview
Adds end-to-end presence UX (badges + status picker + auto-idle) and improves app visibility event plumbing, including sliding-sync presence bootstrapping behavior.
Changes:
- Add presence badges to the compact DM rail and account switcher avatar, plus a Discord-style status picker.
- Implement auto-idle with configurable timeout and introduce a presence REST bootstrap for sliding-sync environments.
- Refactor
appEventsvisibility handling and expanduseAppVisibilitysession-sync/heartbeat logic.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| src/app/utils/appEvents.ts | Replace single visibility handlers with multi-subscriber emit/on API. |
| src/app/state/settings.ts | Add presenceMode setting + ephemeral presenceAutoIdledAtom (also introduces enableMessageBookmarks). |
| src/app/pages/client/sidebar/DirectDMsList.tsx | Render presence badges on compact DM avatars using useUserPresence. |
| src/app/pages/client/sidebar/AccountSwitcherTab.tsx | Add own-presence badge + status picker UI that writes presenceMode. |
| src/app/pages/client/ClientNonUIFeatures.tsx | Drive actual presence broadcasting + auto-idle integration and SW visibility handling. |
| src/app/hooks/useUserPresence.ts | Add sliding-sync REST bootstrap + client-level fallback listener; update labels. |
| src/app/hooks/useUserPresence.test.tsx | New unit tests for useUserPresence. |
| src/app/hooks/usePresenceAutoIdle.ts | New hook implementing inactivity auto-idle + visibility/activity listeners. |
| src/app/hooks/usePresenceAutoIdle.test.tsx | New unit tests for auto-idle behavior and cleanup. |
| src/app/hooks/useClientConfig.ts | Add experiment/sessionSync config types + variant selection helper + presence timeout config. |
| src/app/hooks/useAppVisibility.ts | Rebuild visibility/focus handlers + optional SW session-sync heartbeat behavior. |
| src/app/features/settings/developer-tools/DevelopTools.tsx | Add “Rotate Encryption Sessions” developer tool action. |
| config.json | Add presenceAutoIdleTimeoutMs default (5 min). |
| .changeset/presence-sidebar-badges.md | Changeset entry for presence badges. |
| .changeset/presence-auto-idle.md | Changeset entry for auto-idle + status picker. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
1d06a65 to
2de2315
Compare
b161a41 to
74ca3b4
Compare
Just-Insane
added a commit
to Just-Insane/Sable
that referenced
this pull request
May 13, 2026
…e setting The feat/presence-auto-idle branch (PR SableClient#672) was replaced by feat/presence (PR SableClient#689) without porting the picker, presenceMode setting, DND mode, or usePresenceAutoIdle hook. Restore from the 29e4076 tip still in the local object store. - appEvents: refactor to multi-subscriber Set pattern (supports multiple listeners on onVisibilityChange/onVisibilityHidden) - settings: add presenceMode ('online'|'unavailable'|'dnd'|'offline'), default 'online'; add ephemeral presenceAutoIdledAtom - usePresenceAutoIdle: new hook — inactivity timer, activity reset, appEvents visibility integration, multi-device sync via User.presence - usePresenceAutoIdle.test.tsx: 10 unit tests covering all branches - ClientNonUIFeatures: PresenceFeature now uses presenceMode + autoIdled to broadcast the correct presence state; DND sends status_msg='dnd' - AccountSwitcherTab: own badge driven from presenceMode settings state (not SDK User.presence which MSC4186 servers never echo back); adds Status section to account menu with Online/Idle/DND/Invisible picker
Just-Insane
added a commit
to Just-Insane/Sable
that referenced
this pull request
May 14, 2026
…e setting The feat/presence-auto-idle branch (PR SableClient#672) was replaced by feat/presence (PR SableClient#689) without porting the picker, presenceMode setting, DND mode, or usePresenceAutoIdle hook. Restore from the 29e4076 tip still in the local object store. - appEvents: refactor to multi-subscriber Set pattern (supports multiple listeners on onVisibilityChange/onVisibilityHidden) - settings: add presenceMode ('online'|'unavailable'|'dnd'|'offline'), default 'online'; add ephemeral presenceAutoIdledAtom - usePresenceAutoIdle: new hook — inactivity timer, activity reset, appEvents visibility integration, multi-device sync via User.presence - usePresenceAutoIdle.test.tsx: 10 unit tests covering all branches - ClientNonUIFeatures: PresenceFeature now uses presenceMode + autoIdled to broadcast the correct presence state; DND sends status_msg='dnd' - AccountSwitcherTab: own badge driven from presenceMode settings state (not SDK User.presence which MSC4186 servers never echo back); adds Status section to account menu with Online/Idle/DND/Invisible picker
… members drawer - AccountSwitcherTab: wrap SidebarAvatar in AvatarPresence with current user's dot - DirectDMsList: add AvatarPresence badge on 1:1 DM icons using the DM user's presence - MembersDrawer: replace lastActiveTs !== 0 guard with presence !== Offline so online users show a dot
…e setting The feat/presence-auto-idle branch (PR SableClient#672) was replaced by feat/presence (PR SableClient#689) without porting the picker, presenceMode setting, DND mode, or usePresenceAutoIdle hook. Restore from the 29e4076 tip still in the local object store. - appEvents: refactor to multi-subscriber Set pattern (supports multiple listeners on onVisibilityChange/onVisibilityHidden) - settings: add presenceMode ('online'|'unavailable'|'dnd'|'offline'), default 'online'; add ephemeral presenceAutoIdledAtom - usePresenceAutoIdle: new hook — inactivity timer, activity reset, appEvents visibility integration, multi-device sync via User.presence - usePresenceAutoIdle.test.tsx: 10 unit tests covering all branches - ClientNonUIFeatures: PresenceFeature now uses presenceMode + autoIdled to broadcast the correct presence state; DND sends status_msg='dnd' - AccountSwitcherTab: own badge driven from presenceMode settings state (not SDK User.presence which MSC4186 servers never echo back); adds Status section to account menu with Online/Idle/DND/Invisible picker
MSC4186 servers never echo back the client's own m.presence events, so mx.getUser(myUserId) was never updated in the SDK store. This left own presence badges in the member list stuck at the SDK default forever, and the status editor always showing a blank status. Three changes: - settings: add `presenceStatusMsg` to locally cache the user's custom status message so it survives mode changes and sliding-sync restarts and is no longer silently cleared to '' on every presence broadcast - slidingSync: add SlidingSyncManager.updateOwnPresence(presence, statusMsg) which synthesises a minimal m.presence event for own user and feeds it into the SDK User object — exactly what ExtensionPresence.onResponse does for remote users - ClientNonUIFeatures/PresenceFeature: read presenceStatusMsg from settings, preserve it in mx.setPresence() calls, then call updateOwnPresence() so the local SDK store stays accurate after each broadcast - Profile/handleSaveStatus: persist the new status to the settings atom, call updateOwnPresence() after mx.setPresence() so the member-list badge and status editor reflect the change immediately without needing a server echo
Matrix has no native DND state; Sable encodes it as presence='online' + status_msg='dnd'. Previously only the account switcher decoded this back to a red dot (via a hardcoded Badge); the member list and all other presence badges just saw 'online' and stayed green. - useUserPresence: add Presence.Dnd = 'dnd' to the enum, decode DND in getUserPresence (online + status_msg==='dnd' → Presence.Dnd), add 'Do Not Disturb' label - Presence.tsx: map Presence.Dnd → 'Critical' (red) in PresenceToColor; DND is not Offline so it naturally gets fill='Solid' - AccountSwitcherTab: remove the bespoke DND badge branch; PresenceBadge now handles Presence.Dnd correctly so the own-badge path is unified
8b91958 to
60ab51e
Compare
Three bugs fixed: 1. handleSaveStatus passed presence?.presence to mx.setPresence(). After the Presence.Dnd addition, this could send presence='dnd', which is not a valid Matrix presence value — the server rejects it and the status is silently not saved. Fix: remove the direct mx.setPresence call; PresenceFeature's effect (already triggered by the presenceStatusMsg atom change) handles broadcasting with proper DND translation. 2. getUserPresence exposed user.presenceStatusMsg='dnd' as the status property. The member list and status editor would show the literal text "dnd" for DND users. Fix: filter the sentinel out (return undefined for status when presenceStatusMsg==='dnd'). 3. handleSaveStatus and PresenceFeature's effect both called mx.setPresence concurrently when the status changed. PresenceFeature's call correctly overwrites DND users' custom status with the 'dnd' sentinel, undoing the direct call. Fix: let PresenceFeature be the sole broadcaster; handleSaveStatus eagerly calls updateOwnPresence for instant member-list feedback without waiting for the network round-trip.
60ab51e to
eec0421
Compare
Previously updateOwnPresence() was called inside the .then() callback of mx.setPresence(), so the own-presence badge in the member list and the DND red dot only appeared after the server acknowledged the request. If the server returned an error the update never happened at all. Move updateOwnPresence() before the API call so the local SDK User object is always kept in sync with the settings state immediately. The server PUT still happens in the background for broadcasting to others; we just no longer wait on it to update our own UI.
- Move updateOwnPresence() call to .then() after setPresence() succeeds - Prevents local SDK store from diverging if the network call fails - Improves consistency of own presence badge across UI - Network failures no longer leave stale optimistic state
…n desktop when user is in another app
- useUserPresence: fall back to content.user_id when event has no sender field (MSC4186 sliding sync presence events may omit sender) - MemberTile: show PresenceBadge for any non-offline presence state, not only when a status message is present; move badge to avatar overlay to match MembersDrawer style - sw.ts: add fetchMediaWithCache — checks sable-media-sw-v1 Cache API before the network; caches 2xx responses only so errors are never stored; replaces bare fetch() in all four auth-media code paths - AvatarImage: module-level svgBlobCache so SVG processing runs at most once per URL; subsequent remounts skip async work, eliminating flicker on scroll
…ension Synapse's MSC4186 simplified sliding sync does not implement the presence extension (its get_extensions_response only handles to_device, e2ee, account_data, receipts, typing, and thread_subscriptions), so the extensions.presence field is never included in sync responses. Add a fallback in useUserPresence: if user.lastPresenceTs === 0 (the SDK default — no presence data has ever been received), call mx.getPresence(userId) directly against the REST API to populate the User object. Errors are swallowed so servers with presence disabled silently keep the offline default. The ExtensionPresence class is kept in slidingSync.ts so clients automatically benefit if server support is added later; its comment is updated to document the current server limitation.
…ard in test The bookmarks imports (useInitBookmarks, bookmarksPanelAtom, useReminderSync) leaked from the integration branch and are not used by the presence feature. The usePresenceAutoIdle test dispatched mousemove to simulate activity, but the implementation intentionally ignores mousemove when document.hasFocus() is false (desktop focus guard). jsdom never grants document focus, so the event was silently dropped. Use keydown instead, which has no focus guard.
…1000 desktop) The SW media cache had no size limit — it would grow indefinitely, exhausting iOS Safari's per-origin Cache API quota and triggering PWA storage eviction (causing a full reload on next open). Add FIFO eviction keyed on a platform-aware entry count: 200 on mobile, 1000 on desktop.
…obile / 1000 desktop)" This reverts commit 173cfca.
SW_MEDIA_CACHE and fetchMediaWithCache belong with the other media caching work; this branch retains only the presence-badge changes. Restores mediaFetchConfig (cache: default) for media auth fetches.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds live presence support across the sidebar and account switcher with a Discord-style status picker and auto-idle behaviour.
m.presenceevents, souseUserPresencebootstraps by falling back toGET /presence/{userId}/status(404 silently ignored).presenceModesetting across sessions.presenceAutoIdleTimeoutMsinconfig.json) the client broadcasts Idle and restores the previous status on activity.mousemovewithout OS focus (desktop idle fix), REST API fallback when the MSC4186 sliding sync extension has no presence data.Fixes #
Type of change
Checklist:
AI disclosure:
The REST fallback calls
GET /presence/{userId}/statusand maps thepresencefield to the badge variant. The idle timer subscribes tomousemoveandkeydownondocument, resets on activity, and only fires the Idle broadcast whendocument.hasFocus()to avoid treating background tabs as idle. The optimistic update sets local badge state immediately and rolls back if thePUT /presencecall fails.