diff options
Diffstat (limited to 'src/features')
| -rw-r--r-- | src/features/ark.js | 18 | ||||
| -rw-r--r-- | src/features/messages.js | 12 |
2 files changed, 28 insertions, 2 deletions
diff --git a/src/features/ark.js b/src/features/ark.js index b123458..ba566ee 100644 --- a/src/features/ark.js +++ b/src/features/ark.js @@ -227,6 +227,24 @@ export function mergeArkStates(a, b) { else { lg.claimed = lg.claimed || rg.claimed; lg.revoked = lg.revoked || rg.revoked; } } out.gifts = [...gifts.values()]; + // Targeted spend knowledge beats waiting for a reconcile: a completed + // movement or done action that names its consumed inputs is direct + // evidence those coins are gone. A device waking cold used to merge in + // another device's renewal/send OUTPUT while still counting the consumed + // inputs as spendable — the balance visibly ticked high, then dropped + // when the reconcile caught up. (This is narrow, evidence-based state — + // not the blanket remote-state adoption the rule above forbids.) + const consumed = new Set(); + for (const x of out.actions || []) { + if (x.step !== 'done') continue; + for (const id of x.inputIds || []) consumed.add(id); + for (const part of x.parts || []) if (part.inputId) consumed.add(part.inputId); + } + for (const m of out.movements || []) { + if (m.status !== 'complete') continue; + for (const id of m.inputIds || []) consumed.add(id); + } + for (const v of out.vtxos) if (v.state === 'spendable' && consumed.has(v.id)) v.state = 'spent'; // The checkpoint is READING PROGRESS, not state: adopting a remote one // skips every message this device never processed — balance appears (the // vtxo unions in) but its receive movement and celebration never happen. diff --git a/src/features/messages.js b/src/features/messages.js index ad83389..c1f6d19 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -263,12 +263,20 @@ export function messagesFeature(ctx) { if (cur !== undefined && (cur === null || Date.now() - (cur.t || 0) < PROFILE_TTL)) return cur; profiles.set(pk, cur || null); // null = loading, no fallback art yet fetchNostrProfile(pk).then((p) => { - const entry = { ...(p || {}), t: Date.now() }; + // An empty answer on a cold boot (relays still dialing, the 5s query + // came back with nothing) must never clobber a remembered face with a + // punk — keep the stale fields and just refresh the clock, so the + // known avatar holds while the full-profile fetch does its rounds. + const prev = profiles.get(pk); + const entry = p ? { ...p, t: Date.now() } : { ...(prev || {}), t: Date.now() }; profiles.set(pk, entry); persistProfile(pk, entry); preloadPicture(entry); scheduleRepaint(); - }).catch(() => profiles.set(pk, { t: Date.now() })); + }).catch(() => { + const prev = profiles.get(pk); + profiles.set(pk, { ...(prev || {}), t: Date.now() }); + }); return cur || null; } const displayName = (pk) => { |
