aboutsummaryrefslogtreecommitdiffstats
path: root/src/features/messages.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-11-14 22:36:58 -0700
committerAdam Soltys <[email protected]>2026-11-14 22:36:58 -0700
commit1424cc46382266a51123a8425424f3c8cb943e8e (patch)
treee2b21346781566709e7d0e9c6800b6f812661ce7 /src/features/messages.js
parent933e85020c2cc56b511ff47a73943c92ea1d044a (diff)
downloadcoinosv3-1424cc46382266a51123a8425424f3c8cb943e8e.tar.gz
coinosv3-1424cc46382266a51123a8425424f3c8cb943e8e.zip
A reply, zap or reaction is about the moment of a clip only when you say so. "i zapped and commented on a post from helen yrmom and didn't intend to tie it to the video timestamp but i guess it did by default. maybe we can revisit the ui/flow there so it doesn't do that by default?" (Adam, 2026-11-14). Anything done to a post whose clip had played took the clip's current time: the reply opened as "Comment at 0:12" (an × to drop it), and a zap's or reaction's sheet had nothing showing it at all. Now the time is offered as a toggle, off until tapped — "⏱ About 0:12 in the clip", ✓ when on — in the reply box, the reaction sheet, and both zap sheets (Ark and Lightning); a one-tap zap, which has no sheet, is never tied. Ticked, it goes out as before (the media tag, "[0:12]" before a comment, its mark on the clip's bar). media-moments-feed-test ticks the toggle where it used to rely on the default and checks it starts off; media-timeline, features-smoke and the zap tests pass
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6UKGJyuGyZFxddYRxS
Diffstat (limited to 'src/features/messages.js')
-rw-r--r--src/features/messages.js21
1 files changed, 12 insertions, 9 deletions
diff --git a/src/features/messages.js b/src/features/messages.js
index b9ef1e6..318d1cc 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -1,4 +1,4 @@
-import { mediaMoment, momentTags, readMoment, mediaClock, mediaTimeline, markAt } from '../media-timeline.js';
+import { mediaMoment, momentTags, readMoment, mediaClock, mediaTimeline, markAt, momentToggle } from '../media-timeline.js';
import { mediaServers } from '../media-servers.js';
// Messages — Concord community chat (CORD-01..05) + NIP-17 private DMs.
//
@@ -4834,7 +4834,9 @@ export function messagesFeature(ctx) {
} }, '▶ ' + 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; };
+ // the moment is only offered: a reaction is about it once the toggle says so
+ const withMoment = (ev) => { const m = momentOf(ev); return m ? { ...ev, momentOffer: m } : ev; };
+ const tieLabel = (m) => t('mediaTieTo', { time: mediaClock(m.seconds) });
function attachMediaTimeline(player, url, explicitEvent) {
if (player._timeline) return;
player._timeline = true;
@@ -11279,7 +11281,8 @@ export function messagesFeature(ctx) {
} else ui.noteThread = { rootId: rootIdOf(ev), focusId: ev.id, seed: ev };
const s = ui.noteThread;
ui.profOverThread = false;
- s.mediaMoment = moment;
+ s.momentOffer = moment; // offered, not taken: the toggle in the box ties it
+ s.mediaMoment = null;
s.replying = true; // the box appears under this note
s.scrollPending = false; // focus the composer, without a competing note scroll
s.refocus = true;
@@ -12516,6 +12519,8 @@ export function messagesFeature(ctx) {
},
h('div', { class: 'card col msg-sheet' },
reactRow(null, (e2) => { close(); reactTo(ev, e2).catch(() => {}).finally(after); setTimeout(after, 50); }),
+ ev.momentOffer ? h('div', { class: 'row', style: 'justify-content:center' },
+ momentToggle(h, ev.momentOffer, !!ev.mediaMoment, (on) => { ev.mediaMoment = on ? ev.momentOffer : null; render(); }, tieLabel(ev.momentOffer))) : null,
h('button', { class: 'btn-ghost btn-block', onClick: close }, t('back'))));
}
@@ -13144,10 +13149,8 @@ export function messagesFeature(ctx) {
style: 'gap:8px;margin:0 -8px;padding:' + (joined ? '2px' : '8px') + ' 8px 10px;background:var(--accent-soft,rgba(128,128,128,.08));border-radius:' + (joined ? '0 0 8px 8px' : '8px'),
},
// the moment of the clip this reply is about, and a way to make it about the whole post
- s.mediaMoment ? h('div', { class: 'row small muted', style: 'gap:6px;align-items:center' },
- t('mediaCommentAt') + ' ' + mediaClock(s.mediaMoment.seconds),
- h('button', { type: 'button', class: 'btn-ghost btn-sm', 'aria-label': t('mediaMomentDrop'), title: t('mediaMomentDrop'),
- style: 'padding:0 6px;min-height:0', onClick: () => { s.mediaMoment = null; if (s.momentFor) s.momentFor = null; render(); } }, '×')) : null,
+ s.momentOffer ? h('div', { class: 'row' },
+ momentToggle(h, s.momentOffer, !!s.mediaMoment, (on) => { s.mediaMoment = on ? s.momentOffer : null; render(); }, tieLabel(s.momentOffer))) : null,
s.preview ? draftPreview(s.draft) : null,
mediaBar(),
// A textarea that grows with the reply, like the chat composer: Enter
@@ -13222,7 +13225,7 @@ export function messagesFeature(ctx) {
// morph leaves a focused field's value alone: clear it by hand.
onClick: () => {
const text = s.draft || '';
- s.draft = ''; s.replying = false; s.preview = false; s.mediaMoment = null;
+ s.draft = ''; s.replying = false; s.preview = false; s.mediaMoment = null; s.momentOffer = null;
ui.postMedia = (ui.postMedia || []).filter((m) => !text.includes(m.url));
const inp = document.querySelector('.thread-reply-input');
if (inp) inp.value = '';
@@ -13243,7 +13246,7 @@ export function messagesFeature(ctx) {
// focused field's value — clear it by hand
const inp = document.querySelector('.thread-reply-input');
if (inp) inp.value = '';
- s.replying = false; s.preview = false; s.mediaMoment = null; // sent: the box folds away
+ s.replying = false; s.preview = false; s.mediaMoment = null; s.momentOffer = null; // sent: the box folds away
toast(t('threadReplied'));
} catch (e) { if (!e.silent) toast(e.message); }
s.sending = false; render();