From 74579778e2fe7a095e9d84b4c88ee79ea5d67ca9 Mon Sep 17 00:00:00 2001 From: m Date: Wed, 29 Jul 2026 08:13:31 +1000 Subject: [PATCH] fix(desktop): reload the visible page when switching accounts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `AccountManager.switchAccount()` correctly emits a new `AccountState.LoggedIn` and the `remember(account, ...)` blocks around `iAccount` / `accountRelays` in Main.kt already rebuild those. But the Composables *inside* MainContent — feed screens, notification inbox, profile screen, messages list — all held their own `remember { ... }` state (LazyListState scroll position, expanded rows, per-column filter tabs, in-flight metadata observers, view-models keyed on nothing) that did NOT include the account as a key. So after a profile switch the user still saw account A's feed items, notification unread state, and follow-status overlays rendered under account B's identity, until they navigated away and back to force the column to recompose from scratch. Wrap the `MainContent(...)` call in `key(account.pubKeyHex) { ... }`. This is the idiomatic Compose pattern for "identity changed — tear down the entire subtree and rebuild it fresh": every child's `remember { ... }` block re-runs, every `LaunchedEffect` re-enters, every subscription restarts. Outer state (`deckState`, `workspaceManager`, `singlePaneState`, `pinnedNavBarState`) lives above this call site so the user's column layout / workspace / nav backstack are preserved — only the account-owned content resets. Concretely, after this change: - Home Feed on account A → switch to account B → column now shows account B's home feed, following account B's follow-list. - Notifications tab on A → switch to B → tab reloads with B's unread cursor, B's notification-kind toggles, B's mute/block enforcement. - Direct Messages column stays on the Messages screen, but the chatroom list is B's, DM subscriptions restart against B's kind:10050 inbox relays, giftwrap decryption uses B's signer. - Profile screen viewing @alice: still viewing @alice, but the follow button / mute button reflect B's follow/mute state instead of A's. No new tests: verifying this needs a Compose UI test harness Amethyst Desktop doesn't ship yet. The scoped-teardown behaviour of `key()` is Compose runtime contract, not app-level state to pin down. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../vitorpamplona/amethyst/desktop/Main.kt | 77 +++++++++++-------- 1 file changed, 46 insertions(+), 31 deletions(-) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt index a472cd2b92..6fa4fdd56c 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt @@ -1451,37 +1451,52 @@ private fun AppInner( modifier = bannerModifier, ) Box(modifier = Modifier.weight(1f)) { - MainContent( - layoutMode = layoutMode, - deckState = deckState, - workspaceManager = workspaceManager, - singlePaneState = singlePaneState, - pinnedNavBarState = pinnedNavBarState, - relayManager = relayManager, - localCache = localCache, - accountManager = accountManager, - account = account, - iAccount = iAccount, - accountRelays = accountRelays, - dmSendTracker = dmSendTracker, - nwcConnection = nwcConnection, - subscriptionsCoordinator = subscriptionsCoordinator, - indexRelaysStore = indexRelaysStore, - nip11Fetcher = nip11Fetcher, - dmInboxResolver = dmInboxResolver, - appScope = scope, - torStatus = currentTorStatus, - onShowComposeDialog = onShowComposeDialog, - onShowReplyDialog = onShowReplyDialog, - onEditInComposer = onEditInComposer, - onShowAppDrawer = onShowAppDrawer, - onOpenFeedsDrawer = { - appDrawerInitialTab = - com.vitorpamplona.amethyst.desktop.ui.deck.AppDrawerTab.FEEDS - onShowAppDrawer() - }, - onShowImportFollowListDialog = onShowImportFollowListDialog, - ) + // Force a Compose subtree teardown when the active + // account changes. Without this, the currently-open + // column keeps its account-A `remember { ... }` + // state (LazyListState scroll position, expanded + // rows, filter-tab selection, in-flight metadata + // observers, per-column view-models) even though the + // outer `iAccount` / `accountRelays` swap correctly. + // Users saw account A's notifications / profile / + // messages page rendered under account B's identity + // until they navigated away and back. `key(pubKeyHex)` + // is the idiomatic Compose way to reset an entire + // subtree on identity change while keeping the outer + // deck layout / workspace state (declared above) alive. + androidx.compose.runtime.key(account.pubKeyHex) { + MainContent( + layoutMode = layoutMode, + deckState = deckState, + workspaceManager = workspaceManager, + singlePaneState = singlePaneState, + pinnedNavBarState = pinnedNavBarState, + relayManager = relayManager, + localCache = localCache, + accountManager = accountManager, + account = account, + iAccount = iAccount, + accountRelays = accountRelays, + dmSendTracker = dmSendTracker, + nwcConnection = nwcConnection, + subscriptionsCoordinator = subscriptionsCoordinator, + indexRelaysStore = indexRelaysStore, + nip11Fetcher = nip11Fetcher, + dmInboxResolver = dmInboxResolver, + appScope = scope, + torStatus = currentTorStatus, + onShowComposeDialog = onShowComposeDialog, + onShowReplyDialog = onShowReplyDialog, + onEditInComposer = onEditInComposer, + onShowAppDrawer = onShowAppDrawer, + onOpenFeedsDrawer = { + appDrawerInitialTab = + com.vitorpamplona.amethyst.desktop.ui.deck.AppDrawerTab.FEEDS + onShowAppDrawer() + }, + onShowImportFollowListDialog = onShowImportFollowListDialog, + ) + } } }