aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-29 07:38:08 -0700
committerAdam Soltys <[email protected]>2026-08-29 07:38:08 -0700
commit351aa2248dc1719da1caf845aa8a552461585b23 (patch)
tree1ff809cf9e21543a605b9eefe14c7c66d6addc2f /src
parent7bb047b2211d999f30840a1ba13515fa61b14f08 (diff)
downloadcoinosv3-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
Diffstat (limited to 'src')
-rw-r--r--src/features/messages.js54
-rw-r--r--src/i18n.js1
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.',