mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
[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:
1 parent
b8ac09ef7e
commit
87af650b38
3 files changed
+196
-27
No files matched your search
@@ -650,6 +650,26 @@ export function createAuthSessionCoordinator(deps) {
|
||||
authLoadingState: this.authLoadingState,
|
||||
});
|
||||
|
||||
// Now that the NEW identity's lists (blocks/subscriptions/hashtags) have
|
||||
// settled, backfill the profile modal if it is open — so switching
|
||||
// accounts with the modal visible refreshes the pane the user is viewing
|
||||
// with the freshly-loaded data instead of leaving the previous account's
|
||||
// list on screen until a manual refresh. No-op when the modal is closed.
|
||||
if (
|
||||
this.profileController &&
|
||||
typeof this.profileController.refreshOpenPanesForActiveIdentity ===
|
||||
"function"
|
||||
) {
|
||||
try {
|
||||
this.profileController.refreshOpenPanesForActiveIdentity();
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"[Application] Failed to refresh profile panes after lists settled:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Warm the watch history in the background now that the feed-driving
|
||||
// lists (blocks/subscriptions/hashtags) have finished decrypting. This
|
||||
// runs AFTER the required lists — never concurrently — so it does not
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1368,3 +1368,110 @@ test('handleDirectMessagesRelayWarning suppresses status updates when disabled',
|
||||
controller.handleDirectMessagesRelayWarning(detail);
|
||||
assert.equal(statusCalls.length, 0);
|
||||
});
|
||||
|
||||
// Account-switch refresh (nip-07 -> saved nsec, modal open): the modal must
|
||||
// re-render for the NEW identity without a manual refresh, and a failure in one
|
||||
// identity-reset step must not skip the others (notably the DM reset that clears
|
||||
// the previous account's conversation).
|
||||
//
|
||||
// test_integrity_note:
|
||||
// change_type: ["new_tests"]
|
||||
// scenarios:
|
||||
// - id: SCN-switch-modal-refresh
|
||||
// given: "profile modal controller loaded"
|
||||
// when: "handleAuthLogin runs for a new identity / refreshOpenPanesForActiveIdentity is called"
|
||||
// then: "DM reset still runs after a sibling throw; the active pane is re-run when open"
|
||||
// observable_outcomes:
|
||||
// - "runIdentityResetStep swallows a throwing step (does not propagate)"
|
||||
// - "a throwing DM reset does not skip renderSavedProfiles"
|
||||
// - "refreshOpenPanesForActiveIdentity re-runs selectPane(activePane) only when open"
|
||||
// determinism_controls:
|
||||
// - "instance method spies; no network/timers relied upon for assertions"
|
||||
// anti_cheat_rationale:
|
||||
// prevents: ["over-mocking internal logic", "hard-coded return value"]
|
||||
// relaxation:
|
||||
// did_relax_any_assertion: false
|
||||
|
||||
test('runIdentityResetStep isolates a throwing step (does not propagate)', async () => {
|
||||
const controller = createController();
|
||||
let ran = false;
|
||||
assert.doesNotThrow(() => {
|
||||
controller.runIdentityResetStep('boom', () => {
|
||||
throw new Error('kaboom');
|
||||
});
|
||||
controller.runIdentityResetStep('ok', () => {
|
||||
ran = true;
|
||||
});
|
||||
});
|
||||
assert.equal(ran, true, 'a later reset step still runs after an earlier one throws');
|
||||
});
|
||||
|
||||
test('handleAuthLogin: a throwing DM reset does not skip the saved-profiles reset', async (t) => {
|
||||
const controller = createController();
|
||||
await controller.load();
|
||||
t.after(() => {
|
||||
try {
|
||||
controller.hide({ silent: true });
|
||||
} catch {}
|
||||
resetRuntimeFlags();
|
||||
});
|
||||
|
||||
// Keep the async collaborators inert so the test only exercises the reset order.
|
||||
controller.hydrateActiveWalletSettings = async () => null;
|
||||
controller.refreshAdminPaneState = async () => {};
|
||||
controller.refreshOpenPanesForActiveIdentity = () => {};
|
||||
|
||||
let dmResetPubkey = 'unset';
|
||||
controller.handleActiveDmIdentityChanged = (pubkey) => {
|
||||
dmResetPubkey = pubkey;
|
||||
throw new Error('dm reset blew up');
|
||||
};
|
||||
let savedProfilesRendered = 0;
|
||||
controller.renderSavedProfiles = () => {
|
||||
savedProfilesRendered += 1;
|
||||
};
|
||||
|
||||
const pubkey = 'b'.repeat(64);
|
||||
const result = await controller.handleAuthLogin({
|
||||
pubkey,
|
||||
activeProfilePubkey: pubkey,
|
||||
identityChanged: true,
|
||||
});
|
||||
|
||||
assert.equal(result, true);
|
||||
assert.equal(dmResetPubkey, pubkey, 'DM reset was attempted for the new identity');
|
||||
assert.equal(
|
||||
savedProfilesRendered,
|
||||
1,
|
||||
'the saved-profiles reset still ran even though the DM reset threw',
|
||||
);
|
||||
});
|
||||
|
||||
test('refreshOpenPanesForActiveIdentity re-runs the active pane only when the modal is open', async (t) => {
|
||||
const controller = createController();
|
||||
await controller.load();
|
||||
t.after(() => {
|
||||
try {
|
||||
controller.hide({ silent: true });
|
||||
} catch {}
|
||||
resetRuntimeFlags();
|
||||
});
|
||||
|
||||
const selected = [];
|
||||
controller.selectPane = (name) => {
|
||||
selected.push(name);
|
||||
};
|
||||
controller.getActivePane = () => 'messages';
|
||||
|
||||
controller.isProfileModalOpen = () => false;
|
||||
controller.refreshOpenPanesForActiveIdentity();
|
||||
assert.deepEqual(selected, [], 'closed modal: no pane refresh');
|
||||
|
||||
controller.isProfileModalOpen = () => true;
|
||||
controller.refreshOpenPanesForActiveIdentity();
|
||||
assert.deepEqual(
|
||||
selected,
|
||||
['messages'],
|
||||
'open modal: re-runs the active pane so it reloads for the new identity',
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user