aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-09-15 09:33:36 -0700
committerAdam Soltys <[email protected]>2026-09-15 09:33:36 -0700
commit8f3ef61e4fbaf8c7cf586ed1280fd8f4bdc624ae (patch)
tree086ffb149929f5e1f62136f609b48be0c34f2c04 /src
parentc90d0f3c600802b5b4b43959df5fd2d02f78012c (diff)
downloadcoinosv3-8f3ef61e4fbaf8c7cf586ed1280fd8f4bdc624ae.tar.gz
coinosv3-8f3ef61e4fbaf8c7cf586ed1280fd8f4bdc624ae.zip
A reply's quoted line shows its custom emoji as pictures.
The quote above a reply bubble, the composer's reply bar, and the same on the public /chat page rendered the raw :shortcode:. They now run the one-line snippet through the same NIP-30 tokenizer, cutting on text so a shortcode is never sliced in half. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01JuwQqNWtDJcxPWtEm9134z
Diffstat (limited to 'src')
-rw-r--r--src/features/messages.js36
-rw-r--r--src/public-chat.js9
-rw-r--r--src/style.css2
3 files changed, 39 insertions, 8 deletions
diff --git a/src/features/messages.js b/src/features/messages.js
index 3e45b34..cae2ba7 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -1299,12 +1299,36 @@ export function messagesFeature(ctx) {
}
// The displayed text of a message after its edit fold, trimmed to one line.
- function msgSnippet(room, m, max = 90) {
+ const foldedRumor = (room, m) => {
const edit = room.edits.get(m.rumor.id);
- const text = edit && edit.author === m.author ? edit.rumor.content : m.rumor.content;
+ return edit && edit.author === m.author ? edit.rumor : m.rumor;
+ };
+ const oneLine = (text, max = 90) => {
const one = String(text || '').replace(/\s+/g, ' ').trim();
return one.length > max ? one.slice(0, max - 1) + '…' : one;
+ };
+ function msgSnippet(room, m, max = 90) {
+ return oneLine(foldedRumor(room, m).content, max);
+ }
+ // The snippet as nodes: its custom emoji (NIP-30 tags on the rumor) show
+ // as small pictures, as they do in the bubble being quoted. The cut lands
+ // on the text first, so a shortcode is never sliced in half.
+ function snippetNodes(rumor, max = 90) {
+ const em = emojiTagMap(rumor && rumor.tags);
+ const text = oneLine(rumor && rumor.content, max + 40);
+ if (!em.size) return [oneLine(text, max)];
+ const out = [];
+ let left = max;
+ for (const p of splitEmoji(text, (c) => em.get(c))) {
+ if (left <= 0) { out.push('…'); break; }
+ if (typeof p === 'string') {
+ if (p.length > left) { out.push(p.slice(0, Math.max(0, left - 1)) + '…'); break; }
+ out.push(p); left -= p.length;
+ } else { out.push(emojiImg(p.code, p.url)); left -= 2; }
+ }
+ return out;
}
+ const msgSnippetNodes = (room, m, max = 90) => snippetNodes(foldedRumor(room, m), max);
// A kind-9 that e-tags another message is a REPLY — render the quoted
// context above its text, Telegram-style. Quotes of deleted (or not-yet-
@@ -1316,7 +1340,7 @@ export function messagesFeature(ctx) {
if (!src) return null;
return h('div', { class: 'chat-quote' },
h('span', { class: 'chat-quote-name' }, displayName(src.author)),
- h('span', { class: 'chat-quote-text' }, msgSnippet(room, src)));
+ h('span', { class: 'chat-quote-text' }, ...msgSnippetNodes(room, src)));
}
// Telegram-style message action sheet: quick reactions on top, then the
@@ -1357,7 +1381,7 @@ export function messagesFeature(ctx) {
return h('div', { class: 'reply-bar' },
h('div', { class: 'col grow', style: 'gap:1px;min-width:0' },
h('span', { class: 'small', style: 'font-weight:650' }, '↩ ', displayName(m.author)),
- h('span', { class: 'small muted chat-quote-text' }, msgSnippet(room, m))),
+ h('span', { class: 'small muted chat-quote-text' }, ...msgSnippetNodes(room, m))),
h('button', { class: 'chat-del', style: 'position:static;display:flex;flex-shrink:0', onClick: () => { ui.msgReplyTo = null; render(); } }, '×'));
}
@@ -6071,7 +6095,7 @@ export function messagesFeature(ctx) {
if (!src) return null;
return h('div', { class: 'chat-quote' },
h('span', { class: 'chat-quote-name' }, displayName(src.rumor.pubkey)),
- h('span', { class: 'chat-quote-text' }, String(src.rumor.content || '').replace(/\s+/g, ' ').slice(0, 90)));
+ h('span', { class: 'chat-quote-text' }, ...snippetNodes(src.rumor)));
};
watchZaps(msgs.slice(-150).map((m) => m.rumor.id));
const dmChips = (m) => {
@@ -6094,7 +6118,7 @@ export function messagesFeature(ctx) {
return h('div', { class: 'reply-bar' },
h('div', { class: 'col grow', style: 'gap:1px;min-width:0' },
h('span', { class: 'small', style: 'font-weight:650' }, '↩ ', displayName(m.rumor.pubkey)),
- h('span', { class: 'small muted chat-quote-text' }, String(m.rumor.content || '').replace(/\s+/g, ' ').slice(0, 90))),
+ h('span', { class: 'small muted chat-quote-text' }, ...snippetNodes(m.rumor))),
h('button', { class: 'chat-del', style: 'position:static;display:flex;flex-shrink:0', onClick: () => { ui.msgReplyTo = null; render(); } }, '×'));
};
return h('div', { class: 'card col chat-card chat-page' },
diff --git a/src/public-chat.js b/src/public-chat.js
index 4d79010..91ded5b 100644
--- a/src/public-chat.js
+++ b/src/public-chat.js
@@ -45,6 +45,12 @@ const LINK_SPLIT = /(https?:\/\/[^\s]+|nostr:(?:npub|nprofile|note|nevent|naddr)
const emojiImg = (code, url) => h('img', { class: 'cemoji', src: url, alt: ':' + code + ':', title: ':' + code + ':', loading: 'lazy' });
// `em` is the message's ["emoji", code, url] map; a :code: it names renders
// as its picture, anything else stays text
+// a reply's quoted line: one line, cut at 90, its custom emoji as pictures
+function quoteNodes(rumor) {
+ const em = emojiTagMap(rumor && rumor.tags);
+ const one = String((rumor && rumor.content) || '').replace(/\s+/g, ' ').trim().slice(0, 90);
+ return splitEmoji(one, (c) => em.get(c)).map((p) => (typeof p === 'string' ? p : emojiImg(p.code, p.url)));
+}
function linkedBody(text, em = null) {
const out = [];
const emojiAt = em && em.size ? (c) => em.get(c) : null;
@@ -263,8 +269,7 @@ export function mountPublicChat() {
? (byChannel.get(activeChannel) || new Map()).get(replyId) : null;
const quote = replySrc ? h('div', { class: 'chat-quote' },
h('span', { class: 'chat-quote-name' }, nameOf(replySrc.author)),
- h('span', { class: 'chat-quote-text' },
- String(replySrc.rumor.content || '').replace(/\s+/g, ' ').slice(0, 90))) : null;
+ h('span', { class: 'chat-quote-text' }, ...quoteNodes(replySrc.rumor))) : null;
log.append(h('div', { class: 'chat-row' + (grouped ? ' grouped' : '') },
grouped ? h('div', { class: 'chat-avatar spacer' }) : avatar(m.author),
h('div', { class: 'chat-body' },
diff --git a/src/style.css b/src/style.css
index f969cee..f1670ce 100644
--- a/src/style.css
+++ b/src/style.css
@@ -1043,3 +1043,5 @@ body::after {
in the 40px grid cell that text must not spill into the next cell */
.emoji-grid button { overflow: hidden; }
.emoji-grid .cemoji { font-size: 8px; }
+/* quoted replies and the composer's reply bar run small; their emoji follow */
+.chat-quote-text .cemoji { height: 1.25em; vertical-align: -0.3em; }