diff options
| author | Adam Soltys <[email protected]> | 2026-09-04 09:07:52 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-04 09:07:52 -0700 |
| commit | e43e6dade447bf8ce5150323719937e28ebabe74 (patch) | |
| tree | 62f4a1d2582750fed3a1e8cd21d15f8f3b8df877 /src/public-chat.js | |
| parent | a6851ae954d819b116aae6b19b175525f1488928 (diff) | |
| download | coinosv3-e43e6dade447bf8ce5150323719937e28ebabe74.tar.gz coinosv3-e43e6dade447bf8ce5150323719937e28ebabe74.zip | |
Chat renders links and sends emoji reactions. Links: community bubbles and DM bubbles now run through noteBody (URLs → anchors, image URLs inline, npub mentions clickable, all content kept as text nodes — nothing from a relay reaches innerHTML); the public /chat page gets its own linkedBody (same splitter, nostr refs stay dim stubs since there are no profile screens there). Reactions: a 🙂 opener on every bubble (rides the edge like the delete ×, steps left on own rows) unfolds a six-emoji picker; tapping an existing chip joins that vote, your own reaction is highlighted, and picking a different emoji replaces it (kind-7 rumor on the channel stream, one per author, fold keeps the latest — inbound rendering already existed, this adds the sending half). Optimistic like sendMessage: the chip lands instantly and is rolled back to whatever stood before if signing/wrapping fails
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
Diffstat (limited to 'src/public-chat.js')
| -rw-r--r-- | src/public-chat.js | 26 |
1 files changed, 25 insertions, 1 deletions
diff --git a/src/public-chat.js b/src/public-chat.js index 21b372a..5732f4a 100644 --- a/src/public-chat.js +++ b/src/public-chat.js @@ -35,6 +35,30 @@ function h(tag, attrs = {}, ...children) { return e; } +// Message text, safely: content stays text nodes (relay content must never +// reach innerHTML); URLs become links, image URLs render inline. The app's +// richer noteBody also resolves nostr: mentions — here they stay dim stubs, +// since this page has no profile screens to open. +const LINK_SPLIT = /(https?:\/\/[^\s]+|nostr:(?:npub|nprofile|note|nevent|naddr)1[a-z0-9]+)/gi; +function linkedBody(text) { + const out = []; + for (const part of String(text || '').split(LINK_SPLIT)) { + if (!part) continue; + if (/^https?:\/\//i.test(part)) { + if (/\.(png|jpe?g|gif|webp|avif)(\?[^\s]*)?$/i.test(part)) { + out.push(h('img', { src: part, class: 'note-img', loading: 'lazy', + onError: (e) => { e.target.style.display = 'none'; } })); + } else { + out.push(h('a', { href: part, target: '_blank', rel: 'noopener noreferrer' }, + part.length > 64 ? part.slice(0, 61) + '…' : part)); + } + } else if (/^nostr:/i.test(part)) { + out.push(h('span', { class: 'faint' }, part.slice(6, 18) + '…')); + } else out.push(part); + } + return out; +} + export function mountPublicChat() { const root = document.getElementById('app'); const rootBytes = hexToBytes(COMMUNITY.community_root); @@ -220,7 +244,7 @@ export function mountPublicChat() { h('span', { class: 'chat-name' + (m.author === COMMUNITY.owner ? ' owner' : '') }, nameOf(m.author)), m.author === COMMUNITY.owner ? h('span', { class: 'chat-badge' }, t('msgAdmin')) : null, h('span', { class: 'chat-time' }, timeAgo(ms / 1000))), - h('div', { class: 'chat-bubble' }, content, + h('div', { class: 'chat-bubble' }, ...linkedBody(content), edit && edit.author === m.author ? h('span', { class: 'chat-edited' }, ' ' + t('msgEdited')) : null), reacts && reacts.size ? h('div', { class: 'chat-reacts' }, ...[...[...reacts.values()].reduce((m2, e) => m2.set(e, (m2.get(e) || 0) + 1), new Map()).entries()] |
