Skip to content
Draft
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
223 commits
Select commit Hold shift + click to select a range
7c28e2b
Change GitHub owner from 'SableClient' to 'Just-Insane'
Just-Insane Mar 21, 2026
e977b36
Change default custom domain for Worker
Just-Insane Mar 21, 2026
792fd83
Change default custom domain in variables.tf
Just-Insane Mar 21, 2026
6ab811d
Change default custom domain to dev.cloudhub.social
Just-Insane Mar 21, 2026
a552488
Refactor config.json for new homeserver settings
Just-Insane Mar 21, 2026
58ae0eb
chore: ignore .vscode/launch.json
Just-Insane Mar 25, 2026
ff747a3
ci: build latest Docker image from integration branch too
Just-Insane Mar 25, 2026
024401b
ci: add Sentry env vars to Docker image build step
Just-Insane Mar 25, 2026
c40d5c8
ci: tag integration branch Docker image as 'integration'
Just-Insane Mar 27, 2026
a1aff9e
feat: add pre-push git hook for quality checks
Just-Insane Mar 28, 2026
53a3aff
ci(docker): load env-specific client config overrides
Just-Insane Mar 28, 2026
8366173
ci: integration uses preview env, dev uses production env
Just-Insane Mar 29, 2026
02ced60
ci(workflows): trigger app deploys on config.json changes
Just-Insane Mar 29, 2026
5090322
chore: codespace devcontainer config
Just-Insane Mar 29, 2026
bbb31fc
Update image
Just-Insane Mar 29, 2026
34b1b4d
update startup script
Just-Insane Mar 29, 2026
3edf610
Update setup-signing script
Just-Insane Mar 29, 2026
37b6af1
Updates for ssh
Just-Insane Mar 29, 2026
2ac47de
More script fixes
Just-Insane Mar 29, 2026
68fdb85
more fixes
Just-Insane Mar 29, 2026
6aa40cc
updates
Just-Insane Mar 29, 2026
698bda0
add/setup extensions
Just-Insane Mar 29, 2026
02e9d7a
Merge branch 'SableClient:dev' into personal/config
Just-Insane Mar 29, 2026
2ab3bcd
chore(config): enable phase1 and phase2 session sync flags
Just-Insane Mar 29, 2026
2cf9895
chore(config): add Copilot workspace instructions
Just-Insane Mar 29, 2026
bc6b57d
chore(config): remove devcontainer (setup didn't work out)
Just-Insane Mar 29, 2026
94fae05
Revise GitHub Copilot workspace instructions
Just-Insane Mar 29, 2026
f68ee8e
Update branching instructions for syncing with upstream
Just-Insane Mar 29, 2026
5c4fe1c
Revise instructions for clarity and consistency
Just-Insane Mar 30, 2026
25258f1
Move `copilot-instructions.md` to correct location
Just-Insane Mar 30, 2026
bfee547
Clarify branch creation and PR instructions
Just-Insane Mar 30, 2026
6df45a4
Docs have this location too...
Just-Insane Mar 30, 2026
a5f35e9
chore(config): split copilot-instructions into scoped instruction fil…
Just-Insane Mar 31, 2026
201e230
Update git instructions in AGENTS.md
Just-Insane Mar 31, 2026
04d59f5
Update git commands
Just-Insane Mar 31, 2026
73beb8c
feat(presence): add presence badges to sidebar and fix sliding sync p…
Just-Insane Mar 31, 2026
4a6289e
chore: add changeset for presence-sidebar-badges
Just-Insane Mar 31, 2026
85a7f6d
fix(hooks): handle unhandled rejections in useAsyncCallback
Just-Insane Mar 31, 2026
918f2d7
chore: add changeset for async-callback-rejections
Just-Insane Mar 31, 2026
bc58d6e
feat(flags): inject client config from GH environment variables at build
Just-Insane Mar 29, 2026
add3987
feat(flags): add typed experiment bucketing helper with rollout perce…
Just-Insane Mar 29, 2026
718210f
feat(devtools): add Experiments panel to developer tools settings
Just-Insane Mar 29, 2026
dfebd09
test(flags): cover experiment bucketing and add changeset
Just-Insane Mar 29, 2026
a53112f
feat(polls): implement MSC3381 polls with creator dialog and timeline…
Just-Insane Apr 6, 2026
d9a6656
chore: add changeset for feat-polls
Just-Insane Apr 6, 2026
117949f
feat(bookmarks): add message bookmarks (MSC4438)
Just-Insane Apr 6, 2026
f726833
test(bookmarks): add unit tests for MSC4438 bookmark domain and repos…
Just-Insane Apr 6, 2026
2a9fa52
chore: add changeset for message-bookmarks
Just-Insane Apr 6, 2026
68d35f9
chore(codespace): add devcontainer for iPad browser + SSH signing
Just-Insane Apr 6, 2026
d327e19
chore(codespace): add Fira Code font + ligatures
Just-Insane Apr 6, 2026
426ef74
chore(codespace): split onCreate/postCreate for prebuild caching
Just-Insane Apr 6, 2026
53930d4
chore(codespace): fix image tag, install OMZ+P10k, wire dotfiles bare…
Just-Insane Apr 6, 2026
d9a10cb
fix(codespace): suppress corepack download prompt, source nvm in onCr…
Just-Insane Apr 6, 2026
6361351
fix(codespace): chown pnpm store volume before writing
Just-Insane Apr 6, 2026
3fcd6c1
chore(devcontainer): add tmux, fix terminal font, add GitHub MCP server
Just-Insane Apr 6, 2026
76e0c49
fix(devcontainer): use browser-safe font and compatible p10k glyphs f…
Just-Insane Apr 6, 2026
ff1b207
fix(devcontainer): use Menlo as terminal font for iOS compatibility
Just-Insane Apr 6, 2026
bccc48f
update devcontainer settings
Just-Insane Apr 7, 2026
2964e89
fix(devcontainer): restore missing fontFamily settings
Just-Insane Apr 7, 2026
b3304bb
Update fontfamily
Just-Insane Apr 7, 2026
06c9810
chore(devcontainer): sync extensions list with installed extensions
Just-Insane Apr 7, 2026
ac899aa
Update container config
Just-Insane Apr 7, 2026
dcb56f6
fix(devcontainer): load signing key into ssh-agent in postCreate
Just-Insane Apr 7, 2026
e848f3a
feat(devcontainer): add SSH_AUTH_KEY secret support for server access
Just-Insane Apr 7, 2026
a294d16
fix(devcontainer): disable extension MCP auto-discovery, fix p10k sed…
Just-Insane Apr 7, 2026
2eeaa43
fix(devcontainer): enable shell integration for Copilot Chat terminal
Just-Insane Apr 7, 2026
a4751d4
chore(devcontainer): switch dotfiles branch to codespaces
Just-Insane Apr 8, 2026
1ad6b03
fix(bookmarks): add missing focusId to MSC4438 settings SettingTile
Just-Insane Apr 8, 2026
12888d9
fix(devtools): add focusId to ExperimentsPanel SettingTile
Just-Insane Apr 8, 2026
ac4e5b4
fix(presence): skip REST presence fetch when userId is empty string
Just-Insane Apr 9, 2026
755588b
fix(bookmarks): soft-delete item before updating index in removeBookmark
Just-Insane Apr 9, 2026
49a98b3
feat(polls): expose max_selections in poll creator dialog
Just-Insane Apr 9, 2026
6a27271
test(polls): unit-test pure functions; export extractPollData/compute…
Just-Insane Apr 11, 2026
35acb82
test(presence): add useUserPresence unit tests
Just-Insane Apr 11, 2026
4404e84
feat(presence): add presenceMode setting and Discord-style status picker
Just-Insane Apr 11, 2026
8f69b61
feat(presence): Discord-style presence picker with Idle, DND, and Inv…
Just-Insane Apr 11, 2026
1ba25f8
fix(bookmarks): wire useInitBookmarks and fix orphan tombstoning
Just-Insane Apr 12, 2026
c178b77
feat(presence): add presence badges to sidebar and fix sliding sync p…
Just-Insane Mar 31, 2026
ac75284
chore: add changeset for presence-sidebar-badges
Just-Insane Mar 31, 2026
c7d44d8
fix(presence): skip REST presence fetch when userId is empty string
Just-Insane Apr 9, 2026
ce458fb
test(presence): add useUserPresence unit tests
Just-Insane Apr 11, 2026
f7c7fee
feat(presence): add presenceMode setting and Discord-style status picker
Just-Insane Apr 11, 2026
b86b5de
feat(presence): Discord-style presence picker with Idle, DND, and Inv…
Just-Insane Apr 11, 2026
a71bdab
feat(presence): auto-idle after inactivity timeout
Just-Insane Apr 12, 2026
ca97c9b
chore: add changeset for presence-auto-idle
Just-Insane Apr 12, 2026
594fda0
fix(bookmarks): strip deleted flag on re-add to guarantee re-activation
Just-Insane Apr 12, 2026
31441ad
feat(bookmarks): show Recently Removed section with Restore button
Just-Insane Apr 12, 2026
d8da869
fix: auto-format poll test imports for prettier compliance
Just-Insane Apr 12, 2026
4a15b74
fix: remove unused params from mock Room callbacks
Just-Insane Apr 12, 2026
b0a8091
fix(security): block prototype-polluting keys in deepMerge
Just-Insane Apr 12, 2026
264e4ab
fix(presence): restore missing experiment config helpers and clean pr…
Just-Insane Apr 12, 2026
878f2fc
fix(presence): resolve missing deps and stabilize presence hook tests
Just-Insane Apr 12, 2026
e4d24b2
test(bookmarks): format repository tests for prettier compliance
Just-Insane Apr 13, 2026
05fa657
fix: address PR #589 review comments
Just-Insane Apr 13, 2026
a2d5683
fix(bookmarks): react to item-level account data events and fix remov…
Just-Insane Apr 13, 2026
4eeaa38
feat(room-nav): show topic/last-message preview for space and home rooms
Just-Insane Apr 12, 2026
260a4e8
fix(sliding-sync): increase LIST_TIMELINE_LIMIT to 5 for message prev…
Just-Insane Apr 12, 2026
9036ec9
chore: add changeset for room-message-preview
Just-Insane Apr 12, 2026
db9c1a4
feat(dm-list): show latest message preview below room name
Just-Insane Apr 12, 2026
216aa6a
chore: add changeset for dm message preview
Just-Insane Apr 12, 2026
b848ac2
feat(dm-list): add toggle to hide DM message preview
Just-Insane Apr 12, 2026
ec10020
fix(settings): give DM Message Preview its own card in Visual Tweaks
Just-Insane Apr 12, 2026
20376bf
refactor(sliding-sync): gate listTimelineLimit behind message preview…
Just-Insane Apr 14, 2026
581f2ce
feat(dev-tools): add rotate-sessions developer tool
Just-Insane Mar 31, 2026
86701ca
chore: add changeset for devtool-rotate-sessions
Just-Insane Mar 31, 2026
4b29d29
fix: use || instead of ?? for DM preview fallback chain The nullish …
Just-Insane Apr 14, 2026
2202bec
fix(bookmarks): add Fragment key, guard missing eventId, fix removeBo…
Just-Insane Apr 15, 2026
cd5192b
fix(dev-tools): address review feedback for rotate-sessions
Just-Insane Apr 15, 2026
8cdf36a
chore: remove accidentally committed scratch files
Just-Insane Apr 15, 2026
e5bdd7c
fix(presence): address review feedback for presence sidebar badges
Just-Insane Apr 15, 2026
3f03876
fix(presence): address review feedback for presence-auto-idle
Just-Insane Apr 15, 2026
4cb00a5
fix(room-nav): address review feedback for message preview
Just-Insane Apr 15, 2026
0231581
fix(presence): 5min default, wire visibility reset, add tests
Just-Insane Apr 15, 2026
1f9dae9
test(room-nav): add useRoomLastMessage unit tests (28 tests)
Just-Insane Apr 15, 2026
9d9dce7
fix(polls): handle encrypted poll events in timeline filter
Just-Insane Apr 15, 2026
29e4076
refactor: align presence-auto-idle with sw-push-session-recovery
Just-Insane Apr 15, 2026
12b379e
Merge branch 'feat/presence-sidebar-badges' into feat/presence
Just-Insane Apr 15, 2026
2b39316
Merge branch 'feat/presence-auto-idle' into feat/presence
Just-Insane Apr 15, 2026
69179c1
style: fix lint errors from merge
Just-Insane Apr 15, 2026
9ebd8d3
Merge branch 'dev' into fix/async-callback-rejections
Just-Insane Apr 15, 2026
da4a07b
Merge branch 'dev' into feat/room-message-preview
Just-Insane Apr 15, 2026
d7fd640
fix(presence): address review feedback
Just-Insane Apr 15, 2026
82bde31
fix(dev-tools): add error handling for prepareToEncrypt
Just-Insane Apr 15, 2026
e0eb8ab
Merge branch 'dev' into feat/polls
Just-Insane Apr 15, 2026
7680897
fix(polls): strip invalid vote selections instead of discarding entir…
Just-Insane Apr 15, 2026
850e025
fix(room-nav): use effective event type for decrypted message preview…
Just-Insane Apr 15, 2026
7bb22d2
fix(nav): check DM membership before space parents in useRoomNavigate
Just-Insane Apr 15, 2026
42c59ad
chore(prompts): add rebuild integration and review upstream PRs prompts
Just-Insane Apr 15, 2026
cb24302
chore: fix lint and format issues
Just-Insane Apr 15, 2026
3149a37
chore: fix lint and format issues
Just-Insane Apr 15, 2026
56de896
chore: fix lint and format issues
Just-Insane Apr 15, 2026
ca65cef
docs(changeset): clarify Megolm session rotation description
Just-Insane Apr 15, 2026
cbd653e
docs(changeset): accurately describe unhandled-rejection suppression …
Just-Insane Apr 15, 2026
8fee117
docs: clarify that listTimelineLimit scales with message preview setting
Just-Insane Apr 15, 2026
809a9bb
fix(preview): close decryption race in useRoomLastMessage
Just-Insane Apr 16, 2026
dc21873
fix(preview): poll/location preview, mxid localpart fallback
Just-Insane Apr 16, 2026
8c3c0e7
fix(presence): retry setPresence on failure for app resume reliability
Just-Insane Apr 16, 2026
71138ec
Update personal config.json
Just-Insane Apr 16, 2026
ad50ff1
fix(timeline): restore useLayoutEffect auto-scroll, fix new-message s…
Just-Insane Apr 16, 2026
83031b2
fix(timeline): restore useLayoutEffect auto-scroll, fix new-message s…
Just-Insane Apr 16, 2026
bd31c97
fix(timeline): restore upstream scroll pattern for new messages
Just-Insane Apr 16, 2026
17ccebd
fix(timeline): restore upstream scroll pattern for new messages
Just-Insane Apr 16, 2026
1392b77
fix(timeline): align scrollToBottom with upstream, fix eventId race
Just-Insane Apr 17, 2026
399418b
fix(timeline): align scrollToBottom with upstream, fix eventId race
Just-Insane Apr 17, 2026
700ea1e
Merge branch 'feat/devtool-rotate-sessions' into feat/developer-tools
Just-Insane Apr 17, 2026
272eff3
Merge branch 'feat/feature-flag-env-vars' into feat/developer-tools
Just-Insane Apr 17, 2026
7579368
perf(sidebar): debounce room preview and DM sort updates
Just-Insane Apr 18, 2026
f8986c1
fix(preview): resolve display names in room previews
Just-Insane Apr 18, 2026
5f41827
Merge branch 'dev' into feat/presence
Just-Insane Apr 19, 2026
111d57e
Merge branch 'dev' into feat/room-message-preview
Just-Insane Apr 19, 2026
b60c2dc
Merge branch 'dev' into feat/message-bookmarks
Just-Insane Apr 19, 2026
77f806a
Merge branch 'dev' into feat/polls
Just-Insane Apr 19, 2026
68d653d
Merge branch 'dev' into fix/async-callback-rejections
Just-Insane Apr 19, 2026
5cc0da5
fix(presence): normalize dnd state handling
Just-Insane Apr 19, 2026
e1adb09
fix(preview): remove timeline spillover
Just-Insane Apr 19, 2026
3bea590
fix(bookmarks): remove unrelated branch spillover
Just-Insane Apr 19, 2026
fd9e007
chore(bookmarks): fix branch validation issues
Just-Insane Apr 19, 2026
ef01765
fix(presence): harden desktop auto-idle detection
Just-Insane Apr 19, 2026
1e15b4b
fix(notifications): open joined rooms at live timeline on notificatio…
Just-Insane Apr 13, 2026
164a6b6
fix(notifications): prefer live timeline before event-scoped jump
Just-Insane Apr 13, 2026
143e012
fix(notifications): defer event-scoped jump until event appears in li…
Just-Insane Apr 13, 2026
910d13a
fix(notifications): improve notification jump reliability
Just-Insane Apr 18, 2026
67b1a11
fix(notifications): guarantee jump timeout fallback
Just-Insane Apr 19, 2026
a7f8c1a
fix(sw): reuse preloaded session in handleMinimalPushPayload
Just-Insane Apr 15, 2026
ce68c59
fix(sw): improve push notification reliability and encrypted room han…
Just-Insane Apr 15, 2026
84fadae
fix(sw): add media and notification diagnostics
Just-Insane Apr 19, 2026
c4ca608
fix(timeline): stabilize bottom pin and unread fallback
Just-Insane Apr 19, 2026
157df81
fix(timeline): align initial room-fill thresholds
Just-Insane Apr 19, 2026
be5bcd2
fix(timeline): align reset relinking with upstream
Just-Insane Apr 19, 2026
1fc16ae
fix(notifications): skip in-app notification for active room
Just-Insane Apr 17, 2026
0491959
fix(notifications): pass room and userId context to reaction notifica…
Just-Insane Apr 12, 2026
f6a7cd7
fix(badge): only clear app badge when foregrounded
Just-Insane Apr 16, 2026
2ef5386
fix(notifications): normalize DM room names
Just-Insane Apr 18, 2026
afc2b49
feat(types): add experiment config, sessionSync types and useExperime…
Just-Insane Apr 12, 2026
6b89dc6
fix(sw): increase session TTL to 24h and add requestSessionWithTimeou…
Just-Insane Apr 11, 2026
d863695
fix(sw): reset heartbeat backoff on foreground sync; warm preloadedSe…
Just-Insane Apr 11, 2026
52bbbc6
fix(notifications): restore background visibility sync
Just-Insane Apr 18, 2026
ff50906
fix(sw): recover session sync without controller
Just-Insane Apr 19, 2026
f95c396
test(notifications): align jumper room mock with room utils
Just-Insane Apr 19, 2026
39e25e3
test(notifications): use current jotai hydrate api
Just-Insane Apr 19, 2026
e12a4b0
test(notifications): initialize jumper atoms via jotai store
Just-Insane Apr 19, 2026
ead2bb1
fix(notifications): avoid jumper ref lint error
Just-Insane Apr 19, 2026
e6e6048
fix: kick sliding sync on foreground return
Just-Insane Apr 15, 2026
449de0a
fix(config): enable SW session sync phases for reliable mobile notifi…
Just-Insane Apr 15, 2026
b851f43
feat(cache): unregister service workers on Clear Cache
Just-Insane Apr 16, 2026
a374275
fix(nav): check DM membership before space parents in useRoomNavigate
Just-Insane Apr 15, 2026
21f0f39
Merge branch 'feat/message-bookmarks' into integration
Just-Insane Apr 21, 2026
1e483cc
Merge branch 'feat/polls' into integration
Just-Insane Apr 21, 2026
344536c
Merge branch 'feat/presence' into integration
Just-Insane Apr 21, 2026
0b2b720
Merge branch 'feat/room-message-preview' into integration
Just-Insane Apr 21, 2026
2144458
Merge branch 'fix/async-callback-rejections' into integration
Just-Insane Apr 21, 2026
90aee8c
Merge branch 'fix/notification-context-suppression' into integration
Just-Insane Apr 21, 2026
88840d7
Merge branch 'fix/notification-jump-core' into integration
Just-Insane Apr 21, 2026
93ebd90
Merge branch 'fix/notification-room-labels' into integration
Just-Insane Apr 21, 2026
4896b70
Merge branch 'fix/push-decryption-fallback' into integration
Just-Insane Apr 21, 2026
aeeb6ec
Merge branch 'fix/push-diagnostics' into integration
Just-Insane Apr 21, 2026
0338a34
Merge branch 'fix/sw-session-recovery-core' into integration
Just-Insane Apr 21, 2026
27ac950
Merge branch 'fix/timeline-bottom-pin' into integration-rebuild
Just-Insane Apr 21, 2026
6da27d6
Merge branch 'fix/timeline-reset-alignment' into integration-rebuild
Just-Insane Apr 21, 2026
88b7f05
Merge branch 'fix/cache-sw-unregister' into integration-rebuild
Just-Insane Apr 21, 2026
15911d4
Merge branch 'fix/dm-nav-priority' into integration-rebuild
Just-Insane Apr 21, 2026
8054b69
Merge branch 'personal/config' into integration-rebuild
Just-Insane Apr 21, 2026
ffd63b5
fix(notifications): add registration.active fallback for SW message r…
Just-Insane Apr 21, 2026
3484ec5
fix(timeline): remove duplicate MIN_INITIAL_SCROLL_ROOM_PX declaration
Just-Insane Apr 21, 2026
953c716
fix(timeline): restore missing refs and roomScrollCache utility
Just-Insane Apr 21, 2026
0b9b0fd
fix(lint): resolve all ESLint errors across integration
Just-Insane Apr 21, 2026
95c9993
feat(timeline): configurable message grouping threshold
Just-Insane Apr 22, 2026
f99f9ba
Merge feat/message-grouping into integration
Just-Insane Apr 22, 2026
1eb3847
fix(sw): defer skipWaiting to user-confirmed update prompt
Just-Insane Apr 22, 2026
5ba7bd7
Merge fix/sw-update-skipwaiting into integration
Just-Insane Apr 22, 2026
022649c
fix(room-nav): use stable room ID keys in virtualized room lists
Just-Insane Apr 22, 2026
dbef5d9
Merge fix/virtualizer-room-keys into integration
Just-Insane Apr 22, 2026
a8b17b8
fix(presence): add heartbeat to win back online state on multi-device
Just-Insane Apr 22, 2026
807d00f
Merge feat/presence into integration
Just-Insane Apr 22, 2026
13d1baf
fix(notifications): re-register pusher on mount and catch togglePushe…
Just-Insane Apr 22, 2026
1c50d87
fix(notifications): clear badge on foreground when highlights already…
Just-Insane Apr 22, 2026
708a76a
fix(notifications): restore unread fallback paths for non-DM rooms
Just-Insane Apr 22, 2026
3e373ce
fix(sw): remove 24h session TTL and guard event.data.json()
Just-Insane Apr 24, 2026
d570e4b
fix(sw): require focused+visible to suppress push on iOS
Just-Insane Apr 24, 2026
b2b043c
fix(timeline): restore visible timeline when loadEventTimeline fails
Just-Insane Apr 24, 2026
65b45ba
fix(SyncStatus): only show Connecting... on reconnect, not initial load
Just-Insane Apr 24, 2026
9199a87
fix(sw): guard decrypt-relay suppression with appFocused check
Just-Insane Apr 26, 2026
d82f8e4
revert(SyncStatus): restore upstream connecting banner condition
Just-Insane Apr 26, 2026
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
5 changes: 5 additions & 0 deletions .changeset/presence-auto-idle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: minor
---

feat(presence): add auto-idle presence after configurable inactivity timeout with Discord-style status picker
5 changes: 5 additions & 0 deletions .changeset/presence-sidebar-badges.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: patch
---

Add presence status badges to sidebar DM list and account switcher
2 changes: 2 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
"enabled": true
},

"presenceAutoIdleTimeoutMs": 300000,

"featuredCommunities": {
"openAsDefault": false,
"spaces": [
Expand Down
82 changes: 81 additions & 1 deletion src/app/features/settings/developer-tools/DevelopTools.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useCallback, useState } from 'react';
import { Box, Text, Scroll, Switch, Button } from 'folds';
import { Box, Text, Scroll, Switch, Button, Spinner, color } from 'folds';
import { KnownMembership } from '$types/matrix-sdk';
import { PageContent } from '$components/page';
import { SequenceCard } from '$components/sequence-card';
import { SettingTile } from '$components/setting-tile';
Expand All @@ -9,6 +10,7 @@ import { useMatrixClient } from '$hooks/useMatrixClient';
import { AccountDataEditor, AccountDataSubmitCallback } from '$components/AccountDataEditor';
import { copyToClipboard } from '$utils/dom';
import { SequenceCardStyle } from '$features/settings/styles.css';
import { AsyncStatus, useAsyncCallback } from '$hooks/useAsyncCallback';
import { SettingsSectionPage } from '../SettingsSectionPage';
import { AccountData } from './AccountData';
import { SyncDiagnostics } from './SyncDiagnostics';
Expand All @@ -25,6 +27,33 @@ export function DeveloperTools({ requestBack, requestClose }: DeveloperToolsProp
const [expand, setExpend] = useState(false);
const [accountDataType, setAccountDataType] = useState<string | null>();

const [rotateState, rotateAllSessions] = useAsyncCallback<
{ rotated: number; total: number },
Error,
[]
>(
useCallback(async () => {
const crypto = mx.getCrypto();
if (!crypto) throw new Error('Crypto module not available');

const encryptedRooms = mx
.getRooms()
.filter(
(room) =>
room.getMyMembership() === KnownMembership.Join && mx.isRoomEncrypted(room.roomId)
);

await Promise.all(encryptedRooms.map((room) => crypto.forceDiscardSession(room.roomId)));
const rotated = encryptedRooms.length;

// Proactively start session creation + key sharing with all devices
// (including bridge bots). fire-and-forget per room.
encryptedRooms.forEach((room) => crypto.prepareToEncrypt(room));
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

crypto.prepareToEncrypt(room) is invoked fire-and-forget for each room. If it returns a Promise (or can throw), this can lead to unhandled rejections and makes failures invisible. Consider explicitly voiding and attaching a .catch(...) per call, or awaiting with controlled concurrency if you need to ensure key sharing is actually queued successfully.

Suggested change
// (including bridge bots). fire-and-forget per room.
encryptedRooms.forEach((room) => crypto.prepareToEncrypt(room));
// (including bridge bots). fire-and-forget per room, but surface failures.
encryptedRooms.forEach((room) => {
void Promise.resolve()
.then(() => crypto.prepareToEncrypt(room))
.catch((error) => {
console.error('Failed to prepare room encryption', room.roomId, error);
});
});

Copilot uses AI. Check for mistakes.

return { rotated, total: encryptedRooms.length };
}, [mx])
Comment on lines +31 to +71
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR adds a new Developer Tools action to rotate Megolm sessions across all encrypted rooms, but this functionality isn’t mentioned in the PR description (which is focused on presence). Consider splitting this into a separate PR/changeset to keep scope aligned and reduce risk for the presence feature rollout.

Copilot uses AI. Check for mistakes.
);

const submitAccountData: AccountDataSubmitCallback = useCallback(
async (type, content) => {
// TODO: remove cast once account data typing is unified.
Expand Down Expand Up @@ -109,6 +138,57 @@ export function DeveloperTools({ requestBack, requestClose }: DeveloperToolsProp
)}
</Box>
{developerTools && <SyncDiagnostics />}
{developerTools && (
<Box direction="Column" gap="100">
<Text size="L400">Encryption</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
direction="Column"
gap="400"
>
<SettingTile
title="Rotate Encryption Sessions"
focusId="rotate-encryption-sessions"
description="Discard current Megolm sessions and begin sharing new keys with all room members. Key delivery happens in the background — send a message in each affected room to confirm the bridge has received the new keys."
after={
<Button
onClick={rotateAllSessions}
variant="Secondary"
fill="Soft"
size="300"
radii="300"
outlined
disabled={rotateState.status === AsyncStatus.Loading}
before={
rotateState.status === AsyncStatus.Loading && (
<Spinner size="100" variant="Secondary" />
)
}
>
<Text size="B300">
{rotateState.status === AsyncStatus.Loading ? 'Rotating…' : 'Rotate'}
</Text>
</Button>
}
>
{rotateState.status === AsyncStatus.Success && (
<Text size="T200" style={{ color: color.Success.Main }}>
Sessions discarded for {rotateState.data.rotated} of{' '}
{rotateState.data.total} encrypted rooms. Key sharing is starting in the
background — send a message in an affected room to confirm delivery to
bridges.
</Text>
)}
{rotateState.status === AsyncStatus.Error && (
<Text size="T200" style={{ color: color.Critical.Main }}>
{rotateState.error.message}
</Text>
)}
</SettingTile>
</SequenceCard>
</Box>
)}
{developerTools && (
<AccountData
expand={expand}
Expand Down
219 changes: 198 additions & 21 deletions src/app/hooks/useAppVisibility.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,96 @@
import { useEffect } from 'react';
import { useCallback, useEffect, useRef } from 'react';
import { MatrixClient } from '$types/matrix-sdk';
import { useAtom } from 'jotai';
import { togglePusher } from '../features/settings/notifications/PushNotifications';
import { appEvents } from '../utils/appEvents';
import { useClientConfig } from './useClientConfig';
import { useSetting } from '../state/hooks/settings';
import { settingsAtom } from '../state/settings';
import { pushSubscriptionAtom } from '../state/pushSubscription';
import { mobileOrTablet } from '../utils/user-agent';
import { useClientConfig, useExperimentVariant } from './useClientConfig';
import { createDebugLogger } from '../utils/debugLogger';
import { pushSessionToSW } from '../../sw-session';

const debugLog = createDebugLogger('AppVisibility');

const DEFAULT_FOREGROUND_DEBOUNCE_MS = 1500;
const DEFAULT_HEARTBEAT_INTERVAL_MS = 10 * 60 * 1000;
const DEFAULT_RESUME_HEARTBEAT_SUPPRESS_MS = 60 * 1000;
const DEFAULT_HEARTBEAT_MAX_BACKOFF_MS = 30 * 60 * 1000;

export function useAppVisibility(mx: MatrixClient | undefined) {
const clientConfig = useClientConfig();
const [usePushNotifications] = useSetting(settingsAtom, 'usePushNotifications');
const pushSubAtom = useAtom(pushSubscriptionAtom);
const isMobile = mobileOrTablet();

const sessionSyncConfig = clientConfig.sessionSync;
const sessionSyncVariant = useExperimentVariant(
'sessionSyncStrategy',
mx?.getUserId() ?? undefined
);

// Derive phase flags from experiment variant; fall back to direct config when not in experiment.
const inSessionSync = sessionSyncVariant.inExperiment;
const syncVariant = sessionSyncVariant.variant;
const phase1ForegroundResync = inSessionSync
? syncVariant === 'session-sync-heartbeat' || syncVariant === 'session-sync-adaptive'
: sessionSyncConfig?.phase1ForegroundResync === true;
const phase2VisibleHeartbeat = inSessionSync
? syncVariant === 'session-sync-heartbeat' || syncVariant === 'session-sync-adaptive'
: sessionSyncConfig?.phase2VisibleHeartbeat === true;
const phase3AdaptiveBackoffJitter = inSessionSync
? syncVariant === 'session-sync-adaptive'
: sessionSyncConfig?.phase3AdaptiveBackoffJitter === true;

const foregroundDebounceMs = Math.max(
0,
sessionSyncConfig?.foregroundDebounceMs ?? DEFAULT_FOREGROUND_DEBOUNCE_MS
);
const heartbeatIntervalMs = Math.max(
1000,
sessionSyncConfig?.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS
);
const resumeHeartbeatSuppressMs = Math.max(
0,
sessionSyncConfig?.resumeHeartbeatSuppressMs ?? DEFAULT_RESUME_HEARTBEAT_SUPPRESS_MS
);
const heartbeatMaxBackoffMs = Math.max(
heartbeatIntervalMs,
sessionSyncConfig?.heartbeatMaxBackoffMs ?? DEFAULT_HEARTBEAT_MAX_BACKOFF_MS
);

const lastForegroundPushAtRef = useRef(0);
const suppressHeartbeatUntilRef = useRef(0);
const heartbeatFailuresRef = useRef(0);

const pushSessionNow = useCallback(
(reason: 'foreground' | 'focus' | 'heartbeat'): 'sent' | 'skipped' => {
const baseUrl = mx?.getHomeserverUrl();
const accessToken = mx?.getAccessToken();
const userId = mx?.getUserId();
const canPush =
!!mx &&
typeof baseUrl === 'string' &&
typeof accessToken === 'string' &&
typeof userId === 'string' &&
'serviceWorker' in navigator &&
!!navigator.serviceWorker.controller;

if (!canPush) {
debugLog.warn('network', 'Skipped SW session sync', {
reason,
hasClient: !!mx,
hasBaseUrl: !!baseUrl,
hasAccessToken: !!accessToken,
hasUserId: !!userId,
hasSwController: !!navigator.serviceWorker?.controller,
});
return 'skipped';
}

pushSessionToSW(baseUrl, accessToken, userId);
debugLog.info('network', 'Pushed session to SW', {
reason,
phase1ForegroundResync,
phase2VisibleHeartbeat,
phase3AdaptiveBackoffJitter,
});
return 'sent';
},
[mx, phase1ForegroundResync, phase2VisibleHeartbeat, phase3AdaptiveBackoffJitter]
);

useEffect(() => {
const handleVisibilityChange = () => {
Expand All @@ -26,30 +100,133 @@ export function useAppVisibility(mx: MatrixClient | undefined) {
`App visibility changed: ${isVisible ? 'visible (foreground)' : 'hidden (background)'}`,
{ visibilityState: document.visibilityState }
);
appEvents.onVisibilityChange?.(isVisible);
appEvents.emitVisibilityChange(isVisible);
if (!isVisible) {
appEvents.onVisibilityHidden?.();
appEvents.emitVisibilityHidden();
return;
}

// Always kick the sync loop on foreground regardless of phase flags —
// the SDK may be sitting in exponential backoff after iOS froze the tab.
mx?.retryImmediately();

if (!phase1ForegroundResync) return;

const now = Date.now();
if (now - lastForegroundPushAtRef.current < foregroundDebounceMs) return;
lastForegroundPushAtRef.current = now;

if (pushSessionNow('foreground') === 'sent') {
// A successful push proves the SW controller is up — reset adaptive backoff
// so the heartbeat returns to its normal interval immediately rather than
// staying on an inflated delay left over from a prior SW absence period.
if (phase3AdaptiveBackoffJitter) heartbeatFailuresRef.current = 0;
if (phase3AdaptiveBackoffJitter && phase2VisibleHeartbeat) {
suppressHeartbeatUntilRef.current = now + resumeHeartbeatSuppressMs;
}
}
};

const handleFocus = () => {
if (document.visibilityState !== 'visible') return;

// Always kick the sync loop on focus for the same reason as above.
mx?.retryImmediately();

if (!phase1ForegroundResync) return;

const now = Date.now();
if (now - lastForegroundPushAtRef.current < foregroundDebounceMs) return;
lastForegroundPushAtRef.current = now;

if (pushSessionNow('focus') === 'sent') {
if (phase3AdaptiveBackoffJitter) heartbeatFailuresRef.current = 0;
if (phase3AdaptiveBackoffJitter && phase2VisibleHeartbeat) {
suppressHeartbeatUntilRef.current = now + resumeHeartbeatSuppressMs;
}
}
};

document.addEventListener('visibilitychange', handleVisibilityChange);
window.addEventListener('focus', handleFocus);

return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
window.removeEventListener('focus', handleFocus);
};
}, []);
}, [
foregroundDebounceMs,
mx,
phase1ForegroundResync,
phase2VisibleHeartbeat,
phase3AdaptiveBackoffJitter,
pushSessionNow,
resumeHeartbeatSuppressMs,
]);

useEffect(() => {
if (!mx) return;
if (!phase2VisibleHeartbeat) return undefined;
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The heartbeat effect runs whenever phase2VisibleHeartbeat is enabled, even if mx is currently undefined (ClientRoot calls useAppVisibility(mx) during initial load). That will schedule recurring timers and repeatedly call pushSessionNow('heartbeat'), which will always ‘skipped’ and can generate noisy logs / unnecessary work. Consider guarding the heartbeat loop on mx being defined (and/or only starting once session prerequisites exist).

Suggested change
if (!phase2VisibleHeartbeat) return undefined;
if (!phase2VisibleHeartbeat || !mx) return undefined;

Copilot uses AI. Check for mistakes.

// Reset adaptive backoff/suppression so a config or session change starts fresh.
heartbeatFailuresRef.current = 0;
suppressHeartbeatUntilRef.current = 0;

let timeoutId: number | undefined;

const getDelayMs = (): number => {
let delay = heartbeatIntervalMs;

if (phase3AdaptiveBackoffJitter) {
const failures = heartbeatFailuresRef.current;
const backoffFactor = Math.min(2 ** failures, heartbeatMaxBackoffMs / heartbeatIntervalMs);
delay = Math.min(heartbeatMaxBackoffMs, Math.round(heartbeatIntervalMs * backoffFactor));

// Add +-20% jitter to avoid synchronized heartbeat spikes across many clients.
const jitter = 0.8 + Math.random() * 0.4;
delay = Math.max(1000, Math.round(delay * jitter));
}

return delay;
};

const tick = () => {
const now = Date.now();

const handleVisibilityForNotifications = (isVisible: boolean) => {
togglePusher(mx, clientConfig, isVisible, usePushNotifications, pushSubAtom, isMobile);
if (document.visibilityState !== 'visible' || !navigator.onLine) {
timeoutId = window.setTimeout(tick, getDelayMs());
return;
}

if (phase3AdaptiveBackoffJitter && now < suppressHeartbeatUntilRef.current) {
timeoutId = window.setTimeout(tick, getDelayMs());
return;
}

const result = pushSessionNow('heartbeat');
if (phase3AdaptiveBackoffJitter) {
if (result === 'sent') {
heartbeatFailuresRef.current = 0;
} else {
// 'skipped' means prerequisites (SW controller, session) aren't ready.
// Treat as a transient failure so backoff grows until the SW is ready.
heartbeatFailuresRef.current += 1;
}
}

timeoutId = window.setTimeout(tick, getDelayMs());
};

appEvents.onVisibilityChange = handleVisibilityForNotifications;
// eslint-disable-next-line consistent-return
timeoutId = window.setTimeout(tick, getDelayMs());

return () => {
appEvents.onVisibilityChange = null;
if (timeoutId !== undefined) window.clearTimeout(timeoutId);
};
}, [mx, clientConfig, usePushNotifications, pushSubAtom, isMobile]);
}, [
heartbeatIntervalMs,
heartbeatMaxBackoffMs,
mx,
phase2VisibleHeartbeat,
phase3AdaptiveBackoffJitter,
pushSessionNow,
]);
}
Loading
Loading