aboutsummaryrefslogtreecommitdiffstats
path: root/src/media-timeline.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-20 12:15:32 -0700
committerAdam Soltys <[email protected]>2026-10-20 12:15:32 -0700
commit1e0d551daae8d09f86dbf54ab67bf52610c0c9ac (patch)
tree7e118cc5a49a8308c77b17cbd291012f45cf0875 /src/media-timeline.js
parent84b6a68000c942e17bfe75c1493eddad7ceb5b78 (diff)
downloadcoinosv3-1e0d551daae8d09f86dbf54ab67bf52610c0c9ac.tar.gz
coinosv3-1e0d551daae8d09f86dbf54ab67bf52610c0c9ac.zip
Timed reactions sit on the seek bar at their moment, and the usual buttons carry the time
The "at this time" reply/like/zap buttons are gone, and with them the panel they made over the pager. A reply, like or zap made from a post's own buttons (the feed's, or the pager's ♥ ⚡ and post) while its clip has played is about that moment; the reply box says so and its × makes it about the whole post. What was said, reacted or zapped at a moment sits over the bar at that moment (a custom :emoji: as its picture, from the reaction's own tag; it was showing as :cb-rofl: text), a tap goes there and says who and what, and in the pager it floats up off the bar as the clip plays past. Your reaction no longer turns the count red, only the heart. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
Diffstat (limited to 'src/media-timeline.js')
-rw-r--r--src/media-timeline.js38
1 files changed, 17 insertions, 21 deletions
diff --git a/src/media-timeline.js b/src/media-timeline.js
index bc9f4c7..8b6045e 100644
--- a/src/media-timeline.js
+++ b/src/media-timeline.js
@@ -76,28 +76,26 @@ export function loadWaveform(url) {
return job;
}
+// Where a moment sits along a bar: a percentage of the clip, or null.
+export function markAt(seconds, duration) {
+ return Number.isFinite(duration) && duration > 0 ? Math.min(100, Math.max(0, seconds / duration * 100)) : null;
+}
+
// The live player owns this subtree, so morphing cannot replace its handlers
-// with closures pointing at a detached player.
-export function mediaTimeline(player, url, { h, event, moments, action, label }) {
+// with closures pointing at a detached player. What was said, reacted or
+// zapped at a moment sits on the bar at that moment; replies, reactions and
+// zaps made while a clip plays carry its time on their own (see momentOf).
+export function mediaTimeline(player, url, { h, event, moments, glyph, label }) {
const audio = player.tagName === 'AUDIO';
const box = h('div', { class: 'media-timeline', onClick: e => e.stopPropagation(), onPointerdown: e => e.stopPropagation() });
box._skipMorph = true;
const bars = h('div', { class: 'media-wave', 'aria-hidden': 'true' });
const range = h('input', { type: 'range', min: 0, max: 1000, value: 0, step: 1, 'aria-label': label('mediaSeek'), disabled: true });
- const seek = h('div', { class: 'media-seek' }, bars, range);
- const time = h('span', { class: 'small muted' }, '0:00');
const marks = h('div', { class: 'media-marks' });
+ const seek = h('div', { class: 'media-seek' }, bars, range, marks);
+ const time = h('span', { class: 'small muted media-time' }, '0:00');
const detail = h('div', { class: 'small media-moment-detail', 'aria-live': 'polite' });
- const actions = h('div', { class: 'row media-moment-actions' }, time);
- for (const [kind, icon] of [['comment', '↩'], ['reaction', '♡'], ['zap', '⚡']]) {
- actions.append(h('button', { type: 'button', class: 'btn-sm', 'aria-label': label(kind === 'comment' ? 'msgReply' : kind === 'reaction' ? 'postLike' : 'zapTitle') + ' ' + label('mediaAtTime'), onClick: e => {
- e.stopPropagation();
- const ev = event();
- const m = mediaMoment(url, player.currentTime);
- if (ev && m) action(kind, ev, m, e.currentTarget);
- } }, icon + ' ' + label('mediaAtTime')));
- }
- box.append(seek, actions, marks, detail);
+ box.append(seek, time, detail);
let waveStarted = false, signature = '';
const draw = values => {
if (!values) return;
@@ -112,29 +110,27 @@ export function mediaTimeline(player, url, { h, event, moments, action, label })
const update = () => {
const ev = event();
const finite = Number.isFinite(player.duration) && player.duration > 0;
- actions.hidden = !ev || !finite;
range.disabled = !finite;
range.value = finite ? player.currentTime / player.duration * 1000 : 0;
range.setAttribute('aria-valuetext', mediaClock(player.currentTime) + ' / ' + mediaClock(finite ? player.duration : 0));
seek.style.setProperty('--played', (finite ? player.currentTime / player.duration * 100 : 0) + '%');
time.textContent = mediaClock(player.currentTime) + ' / ' + mediaClock(finite ? player.duration : 0);
- for (const b of actions.querySelectorAll('button')) b.title = b.textContent + ' ' + mediaClock(player.currentTime);
if (audio && !waveStarted && finite) {
waveStarted = true;
const published = waveformFromTags(ev, url);
if (published) draw(published);
else if (player.duration <= 1200 && !navigator.connection?.saveData) loadWaveform(url).then(draw);
}
- const items = ev ? moments(ev.id, url) : [];
- const next = items.map(x => x.id).join(',');
+ const items = ev && finite ? moments(ev.id, url) : [];
+ const next = items.map(x => x.id).join(',') + '@' + (finite ? player.duration : 0);
if (signature !== next) {
signature = next;
- marks.replaceChildren(...items.map(x => h('button', { type: 'button', class: 'media-mark btn-sm',
- title: x.text, onClick: () => { jump(x.moment.seconds); detail.textContent = x.text || x.icon; } }, mediaClock(x.moment.seconds) + ' ' + x.icon)));
+ marks.replaceChildren(...items.map(x => h('button', { type: 'button', class: 'media-mark', title: mediaClock(x.moment.seconds) + ' ' + x.text,
+ style: 'left:' + markAt(x.moment.seconds, player.duration) + '%',
+ onClick: e => { e.stopPropagation(); jump(x.moment.seconds); detail.textContent = mediaClock(x.moment.seconds) + ' ' + x.text; } }, glyph(x))));
}
};
// Fetching/decoding starts only once the user plays (or explicitly seeks).
for (const type of ['timeupdate', 'playing', 'seeked', 'durationchange', 'mediaactivity']) player.addEventListener(type, update);
- actions.hidden = true;
return box;
}