[profile] Refresh the profile modal on account switch (no manual reload)

Switching accounts with the profile modal open (e.g. NIP-07 -> a saved nsec) left
the PREVIOUS account's DMs, subscriptions, hashtags and lists on screen until the
user refreshed. Two problems:

1. handleAuthLogin ran its identity-reset steps unguarded, so if any one threw
   (renderSavedProfiles, a warm-list call, etc.) the steps after it were skipped —
   including handleActiveDmIdentityChanged, which is what clears the previous
   account's conversation. Result: stale DMs after a switch. Each reset step now
   runs in isolation via runIdentityResetStep, and the DM reset runs first.

2. The switch-with-modal-open refresh called populateOpenProfilePanels(), which
   only paints from current service state and omits lazily-loaded panes (messages,
   watch history). It also fired before the new identity's lists had loaded, with
   no reliable refresh afterward. Replaced with refreshOpenPanesForActiveIdentity(),
   which re-runs the ACTIVE pane through selectPane() — the same load+populate path
   as opening it fresh — so DMs reload, subscriptions/hashtags refetch, etc. It runs
   immediately, again once profile/wallet data settles, and once more from the auth
   coordinator when the new identity's blocks/subscriptions/hashtags finish loading
   (backfill for slow sync). Non-active panes stay lazy and repopulate on select.

Per-user list caches (blocks/subscriptions/hashtags are already keyed per pubkey)
mean the active pane shows cached data instantly on switch, then updates when the
new identity's fetch settles — no manual refresh.

Tests (tests/profile-modal-controller.test.mjs +3): a throwing reset step is
isolated; a throwing DM reset no longer skips the saved-profiles reset; and
refreshOpenPanesForActiveIdentity re-runs the active pane only when the modal is
open. Existing profile-modal / switch-coordinator / stored-nsec-switch suites green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-07-01 16:31:01 -04:00
1 parent b8ac09ef7e
commit 87af650b38
3 files changed
+196 -27

No files matched your search

+69 -27
View File
@@ -1258,6 +1258,39 @@ export class ProfileModalController {
void this.refreshDmRelayPreferences({ force: true });
}
// Run one identity-switch reset step in isolation. A failure in a single step
// must not skip the ones after it — notably the DM reset that clears the
// previous account's conversation on a switch.
runIdentityResetStep(label, fn) {
try {
fn();
} catch (error) {
userLogger.warn(
`[profileModal] Identity reset step "${label}" failed:`,
error,
);
}
}
// Refresh the profile modal for the ACTIVE identity while it is open (e.g. right
// after switching accounts). Re-runs the current pane's own lazy load+populate —
// the SAME path as opening the pane fresh — so DMs reload, subscriptions and
// hashtags refetch for the new account, blocked/relays/storage repopulate, etc.
// Non-active panes stay lazy and repopulate when the user selects them.
refreshOpenPanesForActiveIdentity() {
if (!this.isProfileModalOpen()) {
return;
}
try {
this.selectPane(this.getActivePane());
} catch (error) {
userLogger.warn(
"[profileModal] Failed to refresh the active pane for the current identity:",
error,
);
}
}
setMessagesUnreadIndicator(visible) {
if (!(this.dmController.profileMessagesUnreadDot instanceof HTMLElement)) {
return;
@@ -5223,46 +5256,57 @@ export class ProfileModalController {
this.setActivePubkey(activePubkey);
}
// Reset identity-scoped UI state up front, each step isolated so one throwing
// can't skip the others. Regression this guards: switching from NIP-07 to a
// saved nsec (modal open) left the PREVIOUS account's DMs, subscriptions and
// hashtags on screen because a sibling call threw before the DM reset ran.
// DM identity setup also feeds the background unread indicator + DM
// subscription, so it stays eager.
this.runIdentityResetStep("dm-identity", () =>
this.handleActiveDmIdentityChanged(activePubkey),
);
this.runIdentityResetStep("saved-profiles", () => this.renderSavedProfiles());
const walletPromise = this.hydrateActiveWalletSettings(activePubkey);
const adminPromise = this.refreshAdminPaneState().catch((error) => {
userLogger.warn("Failed to refresh admin pane after login:", error);
});
this.renderSavedProfiles();
// Warm blocks + subscriptions, but go through ensureLoaded() (not the raw
// loadBlocks/loadSubscriptions) so these COALESCE with the auth coordinator's
// login-time list loads instead of issuing a second, ungated fetch. The
// direct calls here each kicked off an independent relay round-trip,
// doubling the cold-login kind-30000/10000 REQ burst (KNOWN_BUGS #0).
if (activePubkey) {
if (this.services.userBlocks?.ensureLoaded) {
this.services.userBlocks.ensureLoaded(activePubkey).catch(noop);
} else if (this.services.userBlocks?.loadBlocks) {
this.services.userBlocks.loadBlocks(activePubkey).catch(noop);
}
if (this.subscriptionsService?.ensureLoaded) {
this.subscriptionsService
.ensureLoaded(activePubkey, { allowPermissionPrompt: false })
.catch(noop);
} else if (this.subscriptionsService?.loadSubscriptions) {
this.subscriptionsService
.loadSubscriptions(activePubkey, { allowPermissionPrompt: false })
.catch(noop);
}
this.runIdentityResetStep("warm-lists", () => {
if (this.services.userBlocks?.ensureLoaded) {
this.services.userBlocks.ensureLoaded(activePubkey).catch(noop);
} else if (this.services.userBlocks?.loadBlocks) {
this.services.userBlocks.loadBlocks(activePubkey).catch(noop);
}
if (this.subscriptionsService?.ensureLoaded) {
this.subscriptionsService
.ensureLoaded(activePubkey, { allowPermissionPrompt: false })
.catch(noop);
} else if (this.subscriptionsService?.loadSubscriptions) {
this.subscriptionsService
.loadSubscriptions(activePubkey, { allowPermissionPrompt: false })
.catch(noop);
}
});
}
// DM identity is cheap state setup needed for the background unread
// indicator + DM subscription, so it stays eager.
this.handleActiveDmIdentityChanged(activePubkey);
// Profile-PANEL rendering is deferred: each pane lazily populates when it's
// shown (selectPane), so eagerly rendering all of them at login paints into
// CLOSED panels — friends-list avatars, subscriptions, blocks, storage,
// relays — and jams the main thread right after login (the ~10-15s "profile
// panel won't open / site frozen" report). Only do the eager pass when the
// modal is already open (e.g. switching accounts with it visible);
// otherwise open()/selectPane handles it on demand.
// panel won't open / site frozen" report). Only refresh when the modal is
// already open (e.g. switching accounts with it visible); otherwise
// open()/selectPane handles it on demand. On a switch we re-run the ACTIVE
// pane's own load+populate (refreshOpenPanesForActiveIdentity) so the pane the
// user is viewing reloads for the new identity, then again once profile/wallet
// data settles (list panes also backfill via the auth coordinator once the new
// identity's lists finish loading).
const settleLoginData = Promise.all([
walletPromise,
adminPromise,
@@ -5270,12 +5314,10 @@ export class ProfileModalController {
]);
if (this.isProfileModalOpen()) {
this.populateOpenProfilePanels();
this.refreshOpenPanesForActiveIdentity();
settleLoginData
.then(() => {
if (this.isProfileModalOpen()) {
this.populateOpenProfilePanels();
}
this.refreshOpenPanesForActiveIdentity();
})
.catch((error) => {
userLogger.warn(