diff options
| author | Adam Soltys <[email protected]> | 2026-08-29 07:38:08 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-29 07:38:08 -0700 |
| commit | 351aa2248dc1719da1caf845aa8a552461585b23 (patch) | |
| tree | 1ff809cf9e21543a605b9eefe14c7c66d6addc2f | |
| parent | 7bb047b2211d999f30840a1ba13515fa61b14f08 (diff) | |
| download | coinosv3-351aa2248dc1719da1caf845aa8a552461585b23.tar.gz coinosv3-351aa2248dc1719da1caf845aa8a552461585b23.zip | |
Read is read everywhere: the messages home gains a Mark-all-read link (every DM and channel watermark jumps to its newest foreign message), and read state now rides its own sync domain — watermarks only ever grow, so a per-key max merges cleanly across devices and a thread read on the phone stops glowing on the desk
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019WboeoetMF3MTmgT7Ew8yi
| -rw-r--r-- | src/features/messages.js | 54 | ||||
| -rw-r--r-- | src/i18n.js | 1 |
2 files changed, 54 insertions, 1 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index 8b0f6c9..5f74c46 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -101,6 +101,55 @@ export function messagesFeature(ctx) { if (!ts || (s.read[key] || 0) >= ts) return; s.read[key] = ts; save(s); + // read markers ride their own sync domain (registered below) — nudging + // the cache is what gets the watermark onto the relays for other devices + try { wallet.saveCache(); } catch {} + } + + // One tap, every dot: advance each conversation's watermark to its newest + // foreign message — DMs and community channels alike. + function markAllRead() { + const s = st(); + const my = myPubkeys(); + let changed = false; + const bump = (key, ts) => { if (ts && (s.read[key] || 0) < ts) { s.read[key] = ts; changed = true; } }; + for (const [pk, msgs] of threads) bump(dmRead(pk), newestFrom(msgs.values(), (m) => !m.mine)); + for (const room of rooms.values()) + for (const [id, msgs] of room.byChannel) + bump(chRead(id), newestFrom(msgs.values(), (m) => !my.includes(m.author))); + if (changed) { save(s); try { wallet.saveCache(); } catch {} } + render(); + } + + const anyUnread = () => { + for (const [pk, msgs] of threads) if (dmUnread(pk, msgs)) return true; + for (const room of rooms.values()) if (roomUnread(room)) return true; + return false; + }; + + // Read state syncs across devices: each watermark only ever grows, so a + // per-key max is a clean commutative merge — a thread read anywhere is + // read everywhere. + if (wallet.registerCacheExtension && !wallet._msgReadSync) { + wallet._msgReadSync = true; + wallet.registerCacheExtension({ + domain: 'msgread', + mergeAlways: true, + save: () => { + const read = wallet.loadFeatureState('messages', {}).read || {}; + return Object.keys(read).length ? { msgRead: read } : {}; + }, + load: (d) => { + if (!d.msgRead) return; + const s = wallet.loadFeatureState('messages', {}); + s.read ||= {}; + let changed = false; + for (const [k, v] of Object.entries(d.msgRead)) { + if (typeof v === 'number' && (s.read[k] || 0) < v) { s.read[k] = v; changed = true; } + } + if (changed) wallet.saveFeatureState('messages', s); + }, + }); } // Per-conversation dots for the list screen: newer than when we last had @@ -2076,7 +2125,10 @@ export function messagesFeature(ctx) { // Chat takes the whole screen, so home carries the way back to the wallet. kids.push(h('div', { class: 'row gap6', style: 'align-items:center' }, backBtn(() => { ui.chatOpen = false; render(); }), - h('h3', { style: 'margin:0' }, t('tabMessages')))); + h('h3', { style: 'margin:0' }, t('tabMessages')), + anyUnread() + ? h('button', { class: 'linklike small', style: 'margin-left:auto', onClick: markAllRead }, t('msgMarkAllRead')) + : null)); // A locked wallet can't decrypt or sign — say so up top, where the rooms // that quietly won't open are listed. diff --git a/src/i18n.js b/src/i18n.js index e367393..162883d 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -747,6 +747,7 @@ const STR = { namesZapCodeCopy: 'Copy code', namesOwnDomain: 'Use your own domain', namesOwnDomainHow: 'Have your own domain? Add this line to its DNS records (DNSSEC must be enabled) and your address works there too:', + msgMarkAllRead: 'Mark all read', namesOwnNode: 'Deliver to your own node', namesOwnNodeMenuHow: 'Run a Lightning node? Point your address at it — payments land there instead of in this wallet.', namesOwnNodeHow: 'Run your own Lightning node? Paste the CLINK offer code (noffer1…) from a service beside it — Lightning payments to your address will be delivered straight to your node instead of into this wallet.', |
