aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/features/messages.js40
-rw-r--r--src/features/nostrlogin.js41
-rw-r--r--src/i18n.js4
3 files changed, 75 insertions, 10 deletions
diff --git a/src/features/messages.js b/src/features/messages.js
index 06371cf..597cb23 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -2898,11 +2898,28 @@ export function messagesFeature(ctx) {
// "nothing to wait for": counting those passes gave every wrap sealed to
// the login npub up after ~40 s with only the wallet key tried, and the
// wrap log then hid it from every later session (a phone kept 9 of 186
- // messages). An interactive signer (Amber) is live from the moment it
- // resumes and never decrypts here, so it counts as complete.
+ // messages). An interactive signer (Amber by intent) never decrypts here
+ // either, so it is incomplete too: counted as complete, every wrap sealed
+ // to the login npub was given up on the wallet key and logged as opened
+ // — an Amber user's threads held only what that device had sent.
function decryptorsComplete() {
const login = hook('nostrLoginIdentity');
- return !login || !!(login.signer && (login.signer.interactive || login.signer.decryptFrom));
+ return !login || !!(login.signer && !login.signer.interactive && login.signer.decryptFrom);
+ }
+ const interactiveLogin = () => { const l = hook('nostrLoginIdentity'); return !!(l && l.signer && l.signer.interactive); };
+ // Wraps logged while an interactive signer stood in for the login npub
+ // may be messages no key here ever tried. Once a signer that can decrypt
+ // takes over (Amber reconnected over NIP-46), the heal pass re-opens
+ // every wrap the relays still hold.
+ function noteSignerGap() {
+ const s = st();
+ if (interactiveLogin()) { if (!s.dmSignerGap) { s.dmSignerGap = true; save(s); } return; }
+ if (!s.dmSignerGap || !decryptorsComplete()) return;
+ delete s.dmSignerGap;
+ s.dmHeal = 0;
+ save(s);
+ wrapLog = null; healed.clear(); seenWraps.clear();
+ if (dmStarted) resubscribeStreams();
}
// Raw-key unwraps (seed-derived / pasted nsec — the common case) run in
@@ -3055,6 +3072,7 @@ export function messagesFeature(ctx) {
const pks = dmSubKeys();
if (!pks.length) return;
dmStarted = true;
+ if (interactiveLogin()) noteSignerGap();
loadSeenWraps();
// warm threads from the local cache. A self-thread of our own messages is
// the residue of the old sent-copy misthreading (each welcome DM landed
@@ -12140,6 +12158,17 @@ export function messagesFeature(ctx) {
}, ui.msgReconnecting ? h('span', { class: 'spinner sm' }) : t('msgReconnect')));
}
+ // Amber by intent signs, but each decrypt would be a trip through the app,
+ // so nothing sent TO the login npub is opened here — a thread shows only
+ // what this device sent. Amber also speaks NIP-46; connected that way it
+ // decrypts quietly, and the messages held back open.
+ function amberDmNotice() {
+ if (wallet.watchOnly || !interactiveLogin()) return null;
+ return h('div', { class: 'row gap6 chat-signer-off', style: 'align-items:center' },
+ h('span', { class: 'small muted grow' }, t('msgAmberDms')),
+ h('button', { class: 'btn-sm', onClick: () => hook('nostrSignerUpgrade') }, t('msgAmberConnect')));
+ }
+
// Enter sends; Shift+Enter (the textarea's native edit) and Ctrl+J insert
// a newline. Height follows the text as it's typed — imperatively, since
// typing doesn't re-render — with rows as the re-render fallback.
@@ -12402,6 +12431,8 @@ export function messagesFeature(ctx) {
// that quietly won't open are listed.
const lockedRow = signerNotice();
if (lockedRow && wallet.watchOnly) kids.push(lockedRow);
+ const amberRow = amberDmNotice();
+ if (amberRow) kids.push(amberRow);
if (pendingLink && pendingLink.where !== 'communities') kids.push(linkInviteCard());
for (const [rid, inv] of pendingDirect) kids.push(directInviteCard(rid, inv));
@@ -13319,6 +13350,7 @@ export function messagesFeature(ctx) {
h('div', { class: 'chat-time' }, timeLabel(m.rumor.created_at * 1000)))))
: [h('div', { class: 'muted small', style: 'text-align:center;padding:24px 0' }, t('msgNoDmsYet'))])),
dmReplyBar(),
+ amberDmNotice(),
composer(t('msgDmPlaceholder'), () => sendDM(peer), null, 'dm:' + peer, (f) => sendDMFile(peer, f)),
ui.msgSheet ? dmSheet(peer) : null,
choice && ui.msgRoutePick ? routeSheet(peer, choice) : null);
@@ -13796,7 +13828,7 @@ export function messagesFeature(ctx) {
},
// nostr-login just connected or resumed a signer: wraps the backfill
// couldn't open a moment ago can be opened now — drain right away.
- nostrSignerLive() { scheduleDrain(0); },
+ nostrSignerLive() { noteSignerGap(); scheduleDrain(0); },
// Anyone (ark's history, other features) can open a profile or render a
// small clickable identity chip.
showProfile(pk) { openProfile(pk); return true; },
diff --git a/src/features/nostrlogin.js b/src/features/nostrlogin.js
index 306a12f..2023931 100644
--- a/src/features/nostrlogin.js
+++ b/src/features/nostrlogin.js
@@ -278,6 +278,16 @@ export function nostrLoginFeature(ctx) {
// approval and the login finishes on its own. Deferred a beat so the boot
// restore has decided whether this tab is even logged out.
setTimeout(() => {
+ // the same, for a wallet that was reconnecting its own account (Amber
+ // moving to NIP-46 so messages can be read): back to that screen
+ const again = !nc && wallet.mnemonic && load().pubkey && nostrConnectPending();
+ if (again) {
+ ui.nostrReconnect = 'upgrade';
+ ui.nostrConnectOpen = true;
+ startNostrConnect(reconnectRun, again);
+ render();
+ return;
+ }
const pending = !nc && !wallet.mnemonic && ui.screen === 'unlock' && nostrConnectPending();
if (!pending) return;
if (ui.onb && ['welcome', 'signin'].includes(ui.onb.step)) ui.onb = { step: 'signin' };
@@ -676,6 +686,11 @@ export function nostrLoginFeature(ctx) {
linkSection());
}
+ const reconnectRun = (makeSigner) => reconnectSigner(makeSigner).then(() => {
+ // an upgrade is done only once the new signer can decrypt
+ if (liveSigner() && !(ui.nostrReconnect === 'upgrade' && live.interactive)) { ui.nostrReconnect = null; ui.nostrConnectOpen = false; render(); }
+ });
+
// The reconnect takeover: opened by any feature whose action just found
// the signer missing (a reload dropped it; Amethyst and friends can't
// reattach silently). Same guarded buttons as the settings card — an
@@ -683,15 +698,18 @@ export function nostrLoginFeature(ctx) {
// refuses a different account.
function reconnectScreen() {
const st = load();
- const run = (makeSigner) => reconnectSigner(makeSigner).then(() => {
- if (liveSigner()) { ui.nostrReconnect = null; render(); }
- });
+ // 'upgrade': Amber by intent is live but can't read messages — offer
+ // only its NIP-46 door (nostrconnect), same account enforced
+ const upgrade = ui.nostrReconnect === 'upgrade';
+ const run = reconnectRun;
+ // straight to the link: the screen exists for this one door
+ if (upgrade && ui.nostrUpgradeStart) { ui.nostrUpgradeStart = false; ui.nostrConnectOpen = true; startNostrConnect(run); }
return h('div', { class: 'col', style: 'gap:16px' },
ctx.brandHeader(false),
h('div', { class: 'card col', style: 'gap:12px' },
- h('h3', { style: 'margin:0' }, t('nlReconnectTitle')),
- h('p', { class: 'small muted', style: 'margin:0' }, t('nlReconnectBody', { npub: (npubOf(st.pubkey) || '').slice(0, 12) })),
- signerButtons(run, { reconnectVia: reconnectViaOf(st) }),
+ h('h3', { style: 'margin:0' }, t(upgrade ? 'nlAmberUpgradeTitle' : 'nlReconnectTitle')),
+ h('p', { class: 'small muted', style: 'margin:0' }, upgrade ? t('nlAmberUpgradeBody') : t('nlReconnectBody', { npub: (npubOf(st.pubkey) || '').slice(0, 12) })),
+ upgrade ? nostrConnectSection(run) : signerButtons(run, { reconnectVia: reconnectViaOf(st) }),
ui.nostrLoginError ? h('div', { class: 'notice err' }, ui.nostrLoginError) : null),
h('button', { class: 'btn-ghost btn-block', onClick: () => {
stopNostrConnect();
@@ -710,6 +728,17 @@ export function nostrLoginFeature(ctx) {
// An action needed the login signer and it isn't there: open the
// reconnect screen (and try the silent extension reattach in the
// background — it closes the screen by itself when it lands).
+ // Messages found an Amber-by-intent login that can't decrypt: offer to
+ // reconnect the same account over NIP-46.
+ nostrSignerUpgrade() {
+ const st = load();
+ if (!st.pubkey) return false;
+ ui.nostrReconnect = 'upgrade';
+ ui.nostrUpgradeStart = true;
+ ui.nostrLoginError = '';
+ render();
+ return true;
+ },
nostrReconnectPrompt() {
const st = load();
if (!st.pubkey || liveSigner()) return false;
diff --git a/src/i18n.js b/src/i18n.js
index 32c55b0..e80e2f8 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -515,6 +515,8 @@ const STR = {
addrRead: '{min} min read',
addrList: 'A list by {name}',
msgSignerOff: "Your nostr signer isn't connected, so you can't post as yourself yet.",
+ msgAmberDms: 'Signed in with Amber, messages sent to you can’t be opened here, only ones you sent from this device.',
+ msgAmberConnect: 'Connect Amber',
msgReconnect: 'Reconnect',
msgDmsTitle: 'Messages',
msgCommunitiesTitle: 'Communities',
@@ -1226,6 +1228,8 @@ const STR = {
nlDisconnected: 'not connected',
nlReconnectDesc: "Your signer isn't connected, so this app can't sign as you. Reconnect it to post and update your profile. Your wallet and balance aren't affected.",
nlReconnectTitle: 'Reconnect your signer',
+ nlAmberUpgradeTitle: 'Read your messages',
+ nlAmberUpgradeBody: 'Amber can only open messages here when it’s connected as a remote signer. Tap below, approve in Amber (allow decrypting), and your messages will load. Signing works as before.',
nlReconnectBody: 'Your Nostr signer isn’t connected on this device. Reconnect to keep acting as {npub}…',
nlWrongAccount: 'That signer is a different Nostr account. Reconnect with {npub}.',
nlLinked: 'Wallet linked to your Nostr account',