diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.js | 53 | ||||
| -rw-r--r-- | src/features/messages.js | 39 | ||||
| -rw-r--r-- | src/i18n.js | 4 |
3 files changed, 86 insertions, 10 deletions
@@ -1326,6 +1326,48 @@ function signInAnother() { ui.profilePk = null; ui.profEdit = null; ui.profEditFilled = false; ui.chatOpen = false; render(); } + +// The identities on this device — one per seed (a seed's Spending and +// Savings faces are the same person), each with the nostr key its header +// avatar wears: the nostr login when there is one, else the seed's own key. +// A key is remembered on the account when it's active (saveDirectory) and in +// the durable directory by xpub, so accounts restored from the vault still +// show a face; one never opened on this device has no key yet and falls back +// to its label. +function identities() { + let dir = []; + try { dir = JSON.parse(localStorage.getItem(WATCH_KEY) || '[]'); } catch {} + const dirPk = (a) => (a.xpub && (dir.find((d) => d.xpub === a.xpub) || {}).nostrPk) || null; + const bySeed = new Map(); + for (const a of accounts) { + if (a.provisional) continue; + const key = a.type === 'watch' ? 'w:' + a.xpub + : a.mnemonic ? 'm:' + a.mnemonic + '\n' + (a.passphrase || '') + '\n' + (a.deriveIndex || 0) + : 'x:' + (a.xprv || a.xpub || a.id); + const active = a.id === activeId; + const pk = a.nostrPk || dirPk(a); + const cur = bySeed.get(key); + if (!cur) bySeed.set(key, { id: a.id, pk, active, label: seedName(a), network: a.network, watch: a.type === 'watch' }); + else { + if (active) { cur.id = a.id; cur.active = true; } + if (!cur.pk && pk) cur.pk = pk; + } + } + const out = [...bySeed.values()]; + const me = out.find((x) => x.active); + if (me) me.pk = ctx.shownPubkey() || me.pk; + return out; +} + +// Switch to another identity already signed in on this device: the wallet +// it opens lands on its own history, out of any profile or chat the previous +// identity had open. +function switchIdentity(id) { + const acc = accounts.find((a) => a.id === id); + if (!acc) return; + ui.profilePk = null; ui.profEdit = null; ui.profEditFilled = false; ui.chatOpen = false; + switchToView(acc.id, viewsOf(acc)[0]); +} // The working set of wallets you can switch between. Full (seed-bearing) // accounts live only in sessionStorage — ephemeral, wiped when the browser // closes (no seed on disk by default). Watch-only accounts hold just an xpub, @@ -2584,13 +2626,6 @@ function accountsScreen() { })) ), h('button', { class: 'btn-block', onClick: () => { ui.addWallet = { kind: 'spending', from: 'new' }; render(); } }, t('addWallet')), - // Sign into a DIFFERENT identity (Nostr signer, passkey, Google) without - // logging out: the unlock screen carries the sign-in doors, and enterWallet - // ADDS the account alongside the current ones (fromWallet keeps them + a - // Back). Switching between them afterwards is just tapping a row above. - h('div', { class: 'col', style: 'gap:4px' }, - h('button', { class: 'btn-ghost btn-block', onClick: signInAnother }, t('signInAnother')), - h('div', { class: 'small faint', style: 'padding:0 2px' }, t('signInAnotherNote'))), hasVault() ? h('button', { class: 'btn-ghost btn-block', onClick: startChangePw }, t('changePassword')) : null, h('button', { class: 'btn-ghost btn-block', onClick: () => { ui.confirmClear = true; render(); } }, t('clearAll')) ), @@ -4888,7 +4923,9 @@ const ctx = { // Routes through the Delete-all warning — never a single tap. // The popup has already shown the delete-everything warning by the time // this runs — wipe now and land on the front door. - signInAnother, + // identity switching lives on the profile page (the Accounts screen is + // about Spending/Savings faces, not who you are) + signInAnother, identities, switchIdentity, logoutForget: () => { ui.profilePk = null; ui.profEdit = null; ui.profEditFilled = false; ui.chatOpen = false; ui.pubProf = null; diff --git a/src/features/messages.js b/src/features/messages.js index e85d5d7..574c3c5 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -2314,6 +2314,35 @@ export function messagesFeature(ctx) { onClick: () => { ui.logoutConfirm = 'forget'; render(); }, }, t('logoutForget')) : null, h('button', { class: 'btn-ghost btn-block', onClick: () => { ui.logoutConfirm = null; render(); } }, t('back')))); + // The identity switcher: every account signed in on this device as an + // avatar + name row (tap = switch, nothing logged out), and the door to + // add a new one. + const switchPop = () => { + if (!ui.switchIdPop) return null; + const close = () => { ui.switchIdPop = null; render(); }; + const others = (ctx.identities ? ctx.identities() : []).filter((x) => !x.active); + return h('div', { + class: 'confirm-pop-backdrop', + onClick: (e) => { if (e.target === e.currentTarget) close(); }, + }, + h('div', { class: 'card col confirm-pop', style: 'gap:10px' }, + h('h3', { style: 'margin:0' }, t('switchIdentityTitle')), + h('p', { class: 'small muted', style: 'margin:0' }, t('switchIdentityDesc')), + h('div', { class: 'col', style: 'gap:0' }, + others.map((x) => h('div', { + class: 'row gap6 clickable', style: 'align-items:center;padding:8px 0;border-bottom:1px solid var(--line)', + onClick: () => { ui.switchIdPop = null; ctx.switchIdentity(x.id); }, + }, + x.pk ? avatar(x.pk, 'chat-avatar', false) : h('div', { class: 'chat-avatar fallback' }), + h('div', { class: 'col grow', style: 'min-width:0;gap:1px' }, + h('div', { class: 'chat-name', style: 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap' }, x.pk ? displayName(x.pk) : x.label), + h('div', { class: 'small muted', style: 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap' }, + (x.pk ? x.label : t('identityNoKey')) + + (x.network && x.network !== 'mainnet' ? ' · ' + x.network : '') + + (x.watch ? ' · ' + t('watchOnlyTag') : '')))))), + h('button', { class: 'btn-primary btn-block', onClick: () => { ui.switchIdPop = null; ctx.signInAnother(); } }, t('signInNew')), + h('button', { class: 'btn-ghost btn-block', onClick: close }, t('back')))); + }; const full = fullProfiles.get(pk); // Your own profile reads like anyone else's — npub, bio, latest posts — // with the editor behind an explicit Edit button (it used to BE the @@ -2517,7 +2546,12 @@ export function messagesFeature(ctx) { // on the Accounts screen — logout stays for actually leaving ctx.signInAnother ? h('button', { class: 'btn-block', style: 'display:flex;align-items:center;justify-content:center;gap:8px', - onClick: () => ctx.signInAnother(), + onClick: () => { + // other identities on this device → pick from a list; + // none yet → straight to the sign-in doors + const others = ctx.identities ? ctx.identities().filter((x) => !x.active) : []; + if (others.length) { ui.switchIdPop = true; render(); } else ctx.signInAnother(); + }, }, h('span', { style: 'display:flex', html: '<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="19" y1="8" x2="19" y2="14"/><line x1="22" y1="11" x2="16" y2="11"/></svg>' }), t('signInAnother')) : null, @@ -2573,7 +2607,8 @@ export function messagesFeature(ctx) { : null); })(), h('button', { class: 'btn-ghost btn-block', onClick: () => { ui.profilePk = null; ui.profOverThread = false; ui.pubProf = null; ui.profEdit = null; ui.profEditFilled = false; ui.profCompose = null; render(); } }, t('back')), - mine ? logoutPop() : null); + mine ? logoutPop() : null, + mine ? switchPop() : null); } const backBtn = (onClick) => h('button', { class: 'iconbtn chat-back', onClick }, '‹'); diff --git a/src/i18n.js b/src/i18n.js index 4d11dd8..9d4bf73 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -45,6 +45,10 @@ const STR = { addWallet: 'Add account', signInAnother: 'Sign into another account', signInAnotherNote: 'Add a different identity (Nostr, passkey or Google) without signing out — switch between them here anytime.', + switchIdentityTitle: 'Switch account', + switchIdentityDesc: 'Accounts already signed in on this device. Tap one to switch — nothing is logged out.', + signInNew: 'Sign into a new account', + identityNoKey: 'Not opened on this device yet', clearAll: 'Delete all', logoutForget: 'Logout & forget all data', entropyToggle: 'Entropy', |
