aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/features/messages.js104
1 files changed, 80 insertions, 24 deletions
diff --git a/src/features/messages.js b/src/features/messages.js
index 108af9a..3eb9258 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -564,11 +564,26 @@ export function messagesFeature(ctx) {
const WRAPS_MAX = 1500;
const wrapKey = (id) => String(id).slice(0, 16);
let wrapLog = null, wrapsTimer = 0;
+ // One pass, once per wallet, over the last two weeks of wraps even though
+ // the log calls them opened: caches written before rows kept file fields,
+ // or clobbered by another tab, are missing messages no relay would ever be
+ // asked for again. Only messages and reactions are re-applied.
+ const HEAL_VERSION = 1;
+ let healSince = 0;
+ const healed = new Set();
function loadSeenWraps() {
if (wrapLog) return;
- wrapLog = (st().wraps || []).slice(-WRAPS_MAX);
+ const s = st();
+ wrapLog = (s.wraps || []).slice(-WRAPS_MAX);
for (const k of wrapLog) seenWraps.add(k);
+ healSince = 0;
+ if ((s.dmHeal || 0) < HEAL_VERSION) {
+ if (wrapLog.length) healSince = Math.floor(Date.now() / 1000) - 14 * 86400;
+ s.dmHeal = HEAL_VERSION;
+ save(s);
+ }
}
+ const healing = (wrap) => !!healSince && wrap.created_at >= healSince && !healed.has(wrap.id) && !!healed.add(wrap.id);
function rememberWrap(id) {
const k = wrapKey(id);
if (!wrapLog) wrapLog = [];
@@ -576,7 +591,14 @@ export function messagesFeature(ctx) {
seenWraps.add(k);
clearTimeout(wrapsTimer);
wrapsTimer = setTimeout(() => {
- try { const s = st(); s.wraps = wrapLog.slice(-WRAPS_MAX); wrapLog = s.wraps; save(s); } catch {}
+ try {
+ // another tab may have logged wraps since this one loaded: keep theirs
+ const s = st();
+ const mine = new Set(wrapLog);
+ s.wraps = [...(s.wraps || []).filter((k) => !mine.has(k)), ...wrapLog].slice(-WRAPS_MAX);
+ wrapLog = s.wraps;
+ save(s);
+ } catch {}
}, 1500);
}
let dmStarted = false;
@@ -2778,11 +2800,12 @@ export function messagesFeature(ctx) {
// true: opened. false: not ours. 'later': a remote signer never answered,
// so nothing is known yet — the wrap must be tried again, uncounted.
- async function openInboxWrap(wrap) {
+ async function openInboxWrap(wrap, heal = false) {
for (const d of dmDecryptors()) {
let got;
try { got = await unwrapDMAny(wrap, d); } catch { return 'later'; }
if (!got) continue;
+ if (heal && ![7, 14, 15].includes(got.rumor.kind)) return true;
if (got.rumor.kind === 14 || got.rumor.kind === 15) {
// unwrapDM judges "mine" against the key that DECRYPTED, but this
// wallet can hold several identities (wallet key + nostr login). A
@@ -2824,12 +2847,16 @@ export function messagesFeature(ctx) {
}
async function handleInboxWrap(wrap) {
- if (seenWraps.has(wrap.id) || seenWraps.has(wrapKey(wrap.id))) return;
+ let heal = false;
+ if (seenWraps.has(wrap.id) || seenWraps.has(wrapKey(wrap.id))) {
+ if (!healing(wrap)) return;
+ heal = true;
+ }
seenWraps.add(wrap.id);
- const r = await openInboxWrap(wrap);
+ const r = await openInboxWrap(wrap, heal);
if (r === true) { rememberWrap(wrap.id); return; }
if (pendingWraps.size >= PENDING_MAX) pendingWraps.delete(pendingWraps.keys().next().value);
- pendingWraps.set(wrap.id, { wrap, tries: r !== 'later' && decryptorsComplete() ? 1 : 0 });
+ pendingWraps.set(wrap.id, { wrap, heal, tries: r !== 'later' && decryptorsComplete() ? 1 : 0 });
scheduleDrain(8000);
}
@@ -2862,7 +2889,7 @@ export function messagesFeature(ctx) {
// full-strength failures would evict messages that were never really
// tried. Stop here — the slow retry picks the rest up.
if (complete && !decryptorsComplete()) break;
- const r = await openInboxWrap(p.wrap);
+ const r = await openInboxWrap(p.wrap, p.heal);
if (r === true) { pendingWraps.delete(id); rememberWrap(id); }
else if (r === 'later') break; // the signer went quiet — nothing learned, try the rest later
else if (complete && ++p.tries >= PENDING_TRIES) { pendingWraps.delete(id); rememberWrap(id); }
@@ -2905,11 +2932,7 @@ export function messagesFeature(ctx) {
swept = true;
continue;
}
- for (const m of list)
- threadOf(peer).set(m.id, {
- rumor: { id: m.id, pubkey: m.from, content: m.text, created_at: m.t, kind: 14, tags: (m.em || []).map(([c, u]) => ['emoji', c, u]) },
- mine: isMe(m.from),
- });
+ for (const m of list) threadOf(peer).set(m.id, { rumor: rumorFromRow(m), mine: isMe(m.from) });
}
bumpMsgRev();
if (swept) save(s);
@@ -3033,22 +3056,55 @@ export function messagesFeature(ctx) {
}
}
+ // A cached DM row. A file message (kind 15) keeps its kind and tags — the
+ // url alone is an encrypted blob, and without the key and nonce a reload
+ // turned the picture into a bare link.
+ function rowOf(rumor) {
+ const row = { id: rumor.id, from: rumor.pubkey, text: rumor.content, t: rumor.created_at };
+ if (rumor.kind === 15) {
+ row.k = 15;
+ row.tg = (rumor.tags || []).filter((x) => x[0] !== 'p');
+ } else {
+ // a message's custom emoji ride along, or they'd be text after a reload
+ const em = [...emojiTagMap(rumor.tags)];
+ if (em.length) row.em = em;
+ }
+ return row;
+ }
+ function rumorFromRow(m) {
+ return {
+ id: m.id, pubkey: m.from, content: m.text, created_at: m.t, kind: m.k || 14,
+ tags: m.tg || (m.em || []).map(([c, u]) => ['emoji', c, u]),
+ };
+ }
+
+ // The cache is shared by every tab of this wallet, and each tab only holds
+ // the messages IT has seen: a tab left asleep since yesterday used to
+ // rewrite the cache from its own memory and erase what another tab had
+ // received meanwhile — while the wrap log still said those were opened, so
+ // nothing ever fetched them again. Rows are merged by id with what's
+ // stored; the fuller row (the one carrying file fields) wins.
function persistDms() {
const s = st();
- const byRecent = [...threads.entries()]
- .map(([peer, m]) => [peer, [...m.values()].sort((a, b) => a.rumor.created_at - b.rumor.created_at)])
- .sort((a, b) => (b[1].at(-1)?.rumor.created_at || 0) - (a[1].at(-1)?.rumor.created_at || 0))
+ const merged = new Map(); // peer -> Map(id -> row)
+ for (const [peer, list] of Object.entries(s.dms || {}))
+ merged.set(peer, new Map(list.map((r) => [r.id, r])));
+ for (const [peer, m] of threads) {
+ const rows = merged.get(peer) || merged.set(peer, new Map()).get(peer);
+ for (const e of m.values()) {
+ if (e.pending || e.wn) continue;
+ const row = rowOf(e.rumor);
+ const had = rows.get(row.id);
+ if (!had || !had.tg || row.tg) rows.set(row.id, row);
+ }
+ }
+ const byRecent = [...merged.entries()]
+ .map(([peer, rows]) => [peer, [...rows.values()].sort((a, b) => a.t - b.t)])
+ .filter(([, list]) => list.length)
+ .sort((a, b) => (b[1].at(-1)?.t || 0) - (a[1].at(-1)?.t || 0))
.slice(0, 30);
s.dms = {};
- for (const [peer, list] of byRecent)
- s.dms[peer] = list.filter((m) => !m.pending && !m.wn).slice(-CACHE_MAX)
- .map((m) => {
- const row = { id: m.rumor.id, from: m.rumor.pubkey, text: m.rumor.content, t: m.rumor.created_at };
- // a message's custom emoji ride along, or they'd be text after a reload
- const em = [...emojiTagMap(m.rumor.tags)];
- if (em.length) row.em = em;
- return row;
- });
+ for (const [peer, list] of byRecent) s.dms[peer] = list.slice(-CACHE_MAX);
save(s);
}