aboutsummaryrefslogtreecommitdiffstats
path: root/src/public-chat.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-09-04 09:07:52 -0700
committerAdam Soltys <[email protected]>2026-09-04 09:07:52 -0700
commite43e6dade447bf8ce5150323719937e28ebabe74 (patch)
tree62f4a1d2582750fed3a1e8cd21d15f8f3b8df877 /src/public-chat.js
parenta6851ae954d819b116aae6b19b175525f1488928 (diff)
downloadcoinosv3-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.js26
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()]