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,
|
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
|
// Warm the watch history in the background now that the feed-driving
|
||||||
// lists (blocks/subscriptions/hashtags) have finished decrypting. This
|
// lists (blocks/subscriptions/hashtags) have finished decrypting. This
|
||||||
// runs AFTER the required lists — never concurrently — so it does not
|
// runs AFTER the required lists — never concurrently — so it does not
|
||||||
|
|||||||
@@ -1258,6 +1258,39 @@ export class ProfileModalController {
|
|||||||
void this.refreshDmRelayPreferences({ force: true });
|
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) {
|
setMessagesUnreadIndicator(visible) {
|
||||||
if (!(this.dmController.profileMessagesUnreadDot instanceof HTMLElement)) {
|
if (!(this.dmController.profileMessagesUnreadDot instanceof HTMLElement)) {
|
||||||
return;
|
return;
|
||||||
@@ -5223,46 +5256,57 @@ export class ProfileModalController {
|
|||||||
this.setActivePubkey(activePubkey);
|
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 walletPromise = this.hydrateActiveWalletSettings(activePubkey);
|
||||||
const adminPromise = this.refreshAdminPaneState().catch((error) => {
|
const adminPromise = this.refreshAdminPaneState().catch((error) => {
|
||||||
userLogger.warn("Failed to refresh admin pane after login:", error);
|
userLogger.warn("Failed to refresh admin pane after login:", error);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.renderSavedProfiles();
|
|
||||||
|
|
||||||
// Warm blocks + subscriptions, but go through ensureLoaded() (not the raw
|
// Warm blocks + subscriptions, but go through ensureLoaded() (not the raw
|
||||||
// loadBlocks/loadSubscriptions) so these COALESCE with the auth coordinator's
|
// loadBlocks/loadSubscriptions) so these COALESCE with the auth coordinator's
|
||||||
// login-time list loads instead of issuing a second, ungated fetch. The
|
// login-time list loads instead of issuing a second, ungated fetch. The
|
||||||
// direct calls here each kicked off an independent relay round-trip,
|
// direct calls here each kicked off an independent relay round-trip,
|
||||||
// doubling the cold-login kind-30000/10000 REQ burst (KNOWN_BUGS #0).
|
// doubling the cold-login kind-30000/10000 REQ burst (KNOWN_BUGS #0).
|
||||||
if (activePubkey) {
|
if (activePubkey) {
|
||||||
if (this.services.userBlocks?.ensureLoaded) {
|
this.runIdentityResetStep("warm-lists", () => {
|
||||||
this.services.userBlocks.ensureLoaded(activePubkey).catch(noop);
|
if (this.services.userBlocks?.ensureLoaded) {
|
||||||
} else if (this.services.userBlocks?.loadBlocks) {
|
this.services.userBlocks.ensureLoaded(activePubkey).catch(noop);
|
||||||
this.services.userBlocks.loadBlocks(activePubkey).catch(noop);
|
} else if (this.services.userBlocks?.loadBlocks) {
|
||||||
}
|
this.services.userBlocks.loadBlocks(activePubkey).catch(noop);
|
||||||
if (this.subscriptionsService?.ensureLoaded) {
|
}
|
||||||
this.subscriptionsService
|
if (this.subscriptionsService?.ensureLoaded) {
|
||||||
.ensureLoaded(activePubkey, { allowPermissionPrompt: false })
|
this.subscriptionsService
|
||||||
.catch(noop);
|
.ensureLoaded(activePubkey, { allowPermissionPrompt: false })
|
||||||
} else if (this.subscriptionsService?.loadSubscriptions) {
|
.catch(noop);
|
||||||
this.subscriptionsService
|
} else if (this.subscriptionsService?.loadSubscriptions) {
|
||||||
.loadSubscriptions(activePubkey, { allowPermissionPrompt: false })
|
this.subscriptionsService
|
||||||
.catch(noop);
|
.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
|
// Profile-PANEL rendering is deferred: each pane lazily populates when it's
|
||||||
// shown (selectPane), so eagerly rendering all of them at login paints into
|
// shown (selectPane), so eagerly rendering all of them at login paints into
|
||||||
// CLOSED panels — friends-list avatars, subscriptions, blocks, storage,
|
// CLOSED panels — friends-list avatars, subscriptions, blocks, storage,
|
||||||
// relays — and jams the main thread right after login (the ~10-15s "profile
|
// 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
|
// panel won't open / site frozen" report). Only refresh when the modal is
|
||||||
// modal is already open (e.g. switching accounts with it visible);
|
// already open (e.g. switching accounts with it visible); otherwise
|
||||||
// otherwise open()/selectPane handles it on demand.
|
// 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([
|
const settleLoginData = Promise.all([
|
||||||
walletPromise,
|
walletPromise,
|
||||||
adminPromise,
|
adminPromise,
|
||||||
@@ -5270,12 +5314,10 @@ export class ProfileModalController {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if (this.isProfileModalOpen()) {
|
if (this.isProfileModalOpen()) {
|
||||||
this.populateOpenProfilePanels();
|
this.refreshOpenPanesForActiveIdentity();
|
||||||
settleLoginData
|
settleLoginData
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (this.isProfileModalOpen()) {
|
this.refreshOpenPanesForActiveIdentity();
|
||||||
this.populateOpenProfilePanels();
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
userLogger.warn(
|
userLogger.warn(
|
||||||
|
|||||||
@@ -1368,3 +1368,110 @@ test('handleDirectMessagesRelayWarning suppresses status updates when disabled',
|
|||||||
controller.handleDirectMessagesRelayWarning(detail);
|
controller.handleDirectMessagesRelayWarning(detail);
|
||||||
assert.equal(statusCalls.length, 0);
|
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