aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-26 11:04:51 -0700
committerAdam Soltys <[email protected]>2026-10-26 11:04:51 -0700
commitf5c07454284a123070a0f0159919fc8c2de2b825 (patch)
treeadb0516b9d03250b05cd428ad000e439505e3d8e /src
parentc357e6855fd034c8ccd8dc190df28327e1be4d16 (diff)
downloadcoinosv3-f5c07454284a123070a0f0159919fc8c2de2b825.tar.gz
coinosv3-f5c07454284a123070a0f0159919fc8c2de2b825.zip
A timed comment opens with its time as a chip that plays the clip from there; the "[0:03] " prefix other clients read is left out of the words (and out of the marker's line on the bar)HEADmaster
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
Diffstat (limited to 'src')
-rw-r--r--src/features/messages.js25
-rw-r--r--src/style.css5
2 files changed, 22 insertions, 8 deletions
diff --git a/src/features/messages.js b/src/features/messages.js
index ca560c3..6c6b48d 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -4541,7 +4541,7 @@ export function messagesFeature(ctx) {
const momentsOn = (id, file) => [...(timedActivity.get(id)?.values() || [])]
.filter(x => x.moment.url === mediaMoment(file, 0)?.url && !hiddenPk(x.ev.pubkey) && !isDeleted(x.ev))
.sort((a, b) => a.moment.seconds - b.moment.seconds)
- .map(x => ({ ...x, text: displayName(x.ev.kind === 9735 || x.ev.kind === 9737 ? zapperOf(x.ev) || x.ev.pubkey : x.ev.pubkey) + ' · ' + String(x.text || x.icon).slice(0, 240) }));
+ .map(x => ({ ...x, text: displayName(x.ev.kind === 9735 || x.ev.kind === 9737 ? zapperOf(x.ev) || x.ev.pubkey : x.ev.pubkey) + ' · ' + String(momentStripped(x.ev, x.text) || x.icon).slice(0, 240) }));
// how a moment shows on a bar: a reaction as its emoji (a :code: as its
// picture, from the reaction's own tag), a zap ⚡, a reply as its
// author's face
@@ -4566,6 +4566,19 @@ export function messagesFeature(ctx) {
}
return null;
}
+ // A comment made at a moment of a clip opens with its time as a link that
+ // plays the clip from there; the "[0:03] " other clients read as text is
+ // left out of the words beside it.
+ function momentLink(ev) {
+ const m = readMoment(ev);
+ if (!m) return null;
+ return h('a', { class: 'media-time-link', href: m.url + '#t=' + m.seconds, target: '_blank', rel: 'noopener noreferrer', onClick: e => {
+ e.stopPropagation();
+ const player = [...document.querySelectorAll('audio, video')].find(v => mediaMoment(v.dataset.clip || v.getAttribute('src'), 0)?.url === m.url);
+ if (player) { e.preventDefault(); const seek = () => { player.currentTime = Math.min(m.seconds, Number.isFinite(player.duration) ? player.duration : m.seconds); }; if (player.readyState) seek(); else player.addEventListener('loadedmetadata', seek, { once: true }); player.play().catch(() => {}); player.scrollIntoView({ block: 'center' }); }
+ } }, '▶ ' + mediaClock(m.seconds));
+ }
+ const momentStripped = (ev, text) => (readMoment(ev) ? String(text || '').replace(/^\s*\[\d{1,2}(?::\d{2}){1,2}\]\s*/, '') : text);
const withMoment = (ev) => { const m = momentOf(ev); return m ? { ...ev, mediaMoment: m } : ev; };
function attachMediaTimeline(player, url, explicitEvent) {
if (player._timeline) return;
@@ -12080,13 +12093,9 @@ export function messagesFeature(ctx) {
zapText(ev) ? h('div', {}, ...noteBody(zapText(ev))) : null)
: foreign
? h('div', { class: 'note-text small muted', style: 'white-space:pre-wrap;overflow-wrap:anywhere' }, (ev.kind === 64 ? '\u265f ' : '') + altText)
- : h('div', { class: 'note-text', style: 'white-space:pre-wrap;overflow-wrap:anywhere' }, ...(ev.kind === ARTICLE ? (open ? [articleCard(ev)] : articleBody(ev)) : noteBody(shownTranslation(ev)?.text ?? noteText(ev), 0, noteEmojiTags(ev)))),
- readMoment(ev) ? h('a', { class: 'small media-time-link', href: readMoment(ev).url + '#t=' + readMoment(ev).seconds, target: '_blank', rel: 'noopener noreferrer', onClick: e => {
- e.stopPropagation();
- const m = readMoment(ev);
- const player = [...document.querySelectorAll('audio, video')].find(v => mediaMoment(v.dataset.clip || v.getAttribute('src'), 0)?.url === m.url);
- if (player) { e.preventDefault(); const seek = () => { player.currentTime = Math.min(m.seconds, Number.isFinite(player.duration) ? player.duration : m.seconds); }; if (player.readyState) seek(); else player.addEventListener('loadedmetadata', seek, { once: true }); player.play().catch(() => {}); player.scrollIntoView({ block: 'center' }); }
- } }, '▶ ' + mediaClock(readMoment(ev).seconds)) : null,
+ : h('div', { class: 'note-text', style: 'white-space:pre-wrap;overflow-wrap:anywhere' },
+ momentLink(ev),
+ ...(ev.kind === ARTICLE ? (open ? [articleCard(ev)] : articleBody(ev)) : noteBody(momentStripped(ev, shownTranslation(ev)?.text ?? noteText(ev)), 0, noteEmojiTags(ev)))),
foreign || pending ? null : translateLine(ev),
ev.kind === POLL ? pollBox(ev) : null,
pending ? null : noteActions(pk, ev, { canZap }),
diff --git a/src/style.css b/src/style.css
index e550f25..bf61f6a 100644
--- a/src/style.css
+++ b/src/style.css
@@ -2065,6 +2065,11 @@ html:has(.call-overlay:not(.call-min)) { overflow: hidden; scrollbar-gutter: aut
}
@media (prefers-reduced-motion: reduce) { .mp-pop { animation: none; opacity: 0; } }
+/* a timed comment's time: a chip that plays the clip from there */
+.media-time-link { display: inline-flex; align-items: center; margin-right: 6px; padding: 1px 8px; border-radius: 999px;
+ background: var(--accent-soft); color: var(--accent); font-size: .85em; font-weight: 650; font-variant-numeric: tabular-nums;
+ text-decoration: none; vertical-align: 1px; white-space: nowrap; }
+.media-time-link:hover { background: color-mix(in srgb, var(--accent) 22%, transparent); }
.media-moment-detail { overflow-wrap: anywhere; max-height: 72px; overflow-y: auto; margin-top: 5px; }
.media-moment-detail:empty { display: none; }
.live-watching.has-who { cursor: pointer; text-decoration: underline dotted; text-underline-offset: 3px; }