diff options
| author | Adam Soltys <[email protected]> | 2026-09-14 14:57:27 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-14 14:57:27 -0700 |
| commit | fd3a35a2645b91e70f25f96282d9b8abf8b6562f (patch) | |
| tree | 53eed290c5f0917acad26609fb27491451f50a84 /src | |
| parent | 9a4bc457f3b18afdc7a493672bb1277fa4d956fb (diff) | |
| download | coinosv3-fd3a35a2645b91e70f25f96282d9b8abf8b6562f.tar.gz coinosv3-fd3a35a2645b91e70f25f96282d9b8abf8b6562f.zip | |
A zap on a chat message shows the message, not "Fetching note…" forever.
A receipt for a zap on a chat message or a DM e-tags the rumor's id,
which no relay has ever held as an event — so the row fetched it by id
and waited forever. The rooms and threads we hold are searched first:
the row shows the message's text and a tap opens that room or thread.
A post the relays really don't have says so instead of spinning. And a
coinos zap's own receipt beside the LNURL server's is one row, not two.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QYJfZ4A1TDCEY4Foq3CApR
Diffstat (limited to 'src')
| -rw-r--r-- | src/features/messages.js | 49 | ||||
| -rw-r--r-- | src/i18n.js | 1 |
2 files changed, 42 insertions, 8 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index 9e0c3e9..e39a35e 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -5236,7 +5236,17 @@ export function messagesFeature(ctx) { const known = new Set(c.items.map((x) => x.id)); const add = (evs || []).map(notifItem).filter((x) => x && !known.has(x.id) && known.add(x.id)); if (!add.length) return false; - c.items = [...c.items, ...add].sort((a, b) => b.ts - a.ts).slice(0, NOTIF_KEEP); + // One zap, two receipts: a coinos zap publishes its own (9737) beside the + // LNURL server's (9735). The same person, post and amount within a few + // minutes is one row. + const all = [...c.items, ...add].sort((a, b) => b.ts - a.ts); + const kept = []; + for (const x of all) { + if (x.what === 'zap' && kept.some((y) => y.what === 'zap' && y.actor === x.actor && y.target === x.target + && y.sats === x.sats && Math.abs(y.ts - x.ts) < 300)) continue; + kept.push(x); + } + c.items = kept.slice(0, NOTIF_KEEP); // the rows already say who; a reply row also wants its reader-facing text, // which it carries — the events themselves are not kept const s2 = st(); s2.notifs = c.items; save(s2); @@ -5281,6 +5291,24 @@ export function messagesFeature(ctx) { const q = quotedNote({ id, relays: [] }); return q.ev || null; } + // A zap can land on a chat message or a DM rather than a post — the + // receipt e-tags the rumor's id, which no relay has ever seen as an event, + // so fetching it by id would spin forever. Look in the rooms and threads + // we hold first; that is where the text is, and where a tap should go. + function messageInHand(id) { + for (const [cid, room] of rooms) { + for (const [chId, msgs] of room.byChannel) { + const m = msgs.get(id); + if (m) return { cid, chId, text: String(m.rumor.content || '') }; + } + } + for (const [peer, msgs] of threads) { + const m = msgs.get(id); + if (m) return { peer, text: String(m.rumor.content || '') }; + } + return null; + } + const targetGone = (id) => { const q = quoted.get(id); return !!q && q.status === 'missing'; }; // A one-line excerpt: mentions read as names, not as nostr:nprofile1… keys. function plainExcerpt(text) { return String(text || '').replace(/nostr:(npub|nprofile)1[a-z0-9]+/gi, (m) => { @@ -5297,16 +5325,21 @@ export function messagesFeature(ctx) { } function notifRow(x) { profileOf(x.actor); - const target = x.what === 'reply' || x.what === 'mention' ? null : notifTarget(x.target); - const excerpt = x.what === 'reply' || x.what === 'mention' + const aboutNote = !(x.what === 'reply' || x.what === 'mention'); + const msg = aboutNote ? messageInHand(x.target) : null; + const target = aboutNote && !msg ? notifTarget(x.target) : null; + const excerpt = !aboutNote ? plainExcerpt(x.text) - : target ? plainExcerpt(String(target.content || '').slice(0, 140)) : null; + : msg ? plainExcerpt(msg.text.slice(0, 140)) + : target ? plainExcerpt(String(target.content || '').slice(0, 140)) : null; const open = () => { - if (x.what === 'reply' || x.what === 'mention') { + if (!aboutNote) { const ev = notifNotes.get(x.id); if (ev) openNoteThread(ev); else openNoteRef({ id: x.id }).catch(() => {}); - } else if (target) openNoteThread(target); - else if (x.target) openNoteRef({ id: x.target }).catch(() => {}); + } else if (msg && msg.peer) { ui.msgView = 'dm'; ui.msgPeer = msg.peer; ui.msgStick = true; stopNotifWatch(); render(); } + else if (msg) { ui.msgView = 'room'; ui.msgCommunity = msg.cid; ui.msgChannel = msg.chId; ui.msgStick = true; stopNotifWatch(); render(); } + else if (target) openNoteThread(target); + else if (x.target && !targetGone(x.target)) openNoteRef({ id: x.target }).catch(() => {}); }; return h('div', { class: 'row alert-row' + (x.ts > notifSeenAtOpen ? ' fresh' : ''), @@ -5323,7 +5356,7 @@ export function messagesFeature(ctx) { x.what === 'zap' && x.text ? h('div', { class: 'small' }, x.text) : null, excerpt != null ? h('div', { class: 'small muted alert-excerpt' }, excerpt) - : x.target ? h('div', { class: 'small faint' }, t('noteRefLoading')) : null)); + : x.target ? h('div', { class: 'small faint' }, targetGone(x.target) ? t('alertNoteGone') : t('noteRefLoading')) : null)); } let notifSeenAtOpen = 0; // what counted as new when the list was opened function openNotifs() { diff --git a/src/i18n.js b/src/i18n.js index 07a545c..290b514 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -211,6 +211,7 @@ const STR = { alertBoost: 'boosted your post', alertReply: 'replied to you', alertMention: 'mentioned you', + alertNoteGone: 'The post is no longer on your relays', postWho: 'Who reacted', feedFollow: 'Follow', feedFollowing: 'Following', |
