diff options
Diffstat (limited to 'src/features')
| -rw-r--r-- | src/features/messages.js | 56 |
1 files changed, 45 insertions, 11 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index ad5c32c..2bc034f 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -12284,11 +12284,31 @@ export function messagesFeature(ctx) { if (fsElement() === document.documentElement) exitFs(); } // The words of the post, without the links to the media it carries. - function mediaCaption(ev) { + function mediaCaption(ev, raw = false) { let s = isVideoEvent(ev) ? videoEventText(ev) : String(ev.content || ''); s = s.replace(new RegExp(MD_LINK, 'gi'), (m) => (MD_PARTS.exec(m)?.[1] ? '' : m)); for (const u of [...noteMediaUrls(s), ...noteVideoUrls(s)]) s = s.split(u).join(''); - return plainExcerpt(s); + return raw ? s.replace(/\s+/g, ' ').trim() : plainExcerpt(s); + } + // The pager's caption: the post's words with their names, tags and links + // live (a plain excerpt left "@NostrValley" dead — Adam, 2026-10-16). A + // tap on a name, a tag or a note reference leaves the pager, and the full + // screen with it, before going where it goes; a link to the web opens in + // its own tab and the pager stays. A tap anywhere else on the caption + // leaves the pager and opens the post ("clicking anywhere on the caption + // that's not a link should take me out of fullscreen and take me to the + // post"), where the whole of a long caption reads. + function captionNode(ev, text) { + const el = h('div', { class: 'mp-caption' }, ...noteBody(text, 2, noteEmojiTags(ev))); + el.addEventListener('click', (e) => { + const a = e.target && e.target.closest ? e.target.closest('a') : null; + if (a && /^https?:/i.test(a.getAttribute('href') || '')) { e.stopPropagation(); return; } + leavePager(); + if (a) return; // the link's own handler opens the name, the tag, the note + e.stopPropagation(); e.preventDefault(); + openNoteThread(ev); + }, true); + return el; } function setMediaMode(m) { mediaMode = m; @@ -12549,9 +12569,21 @@ export function messagesFeature(ctx) { // and the flight stuttered (Adam, 2026-10-16: "a bit of choppiness as // they scroll"). A copy that fails falls back to the original. const quick = !video && !/\.gif(\?|$)/i.test(url) ? (mediaReady.get(url) || {}).thumb || serverPic(url, 'col') || url : url; + // A clip that will play by itself has no poster: every poster there is + // (the kept frame, a post's imeta picture, the server's) is 480px wide + // and taken at some moment of the clip, so it showed for a beat, soft + // and not quite the first frame, and the clip's own first frame + // replaced it — a flash on every flick (Adam, 2026-10-16: "if we're + // auto playing let's not show the thumbnail let's just start on the + // first frame of the video"). The slide is the pager's black until the + // player, whose opening is in the cache by then (headStart), has its + // first frame. (Giving the next clip's player its file ahead of time + // was tried: it pulled the whole file down.) On a connection to be + // careful with the poster stays: the clip may never load (a heavy one + // waits for a tap), and the still is what there is. const media = video ? h('video', { class: 'mp-media', 'data-src': url, loop: true, playsinline: true, preload: 'none', - poster: videoThumbs.get(url) || posterOf(ev, url) || (videoInfo.get(url) || {}).image || serverThumb(url) }) + poster: frugalData() ? videoThumbs.get(url) || posterOf(ev, url) || (videoInfo.get(url) || {}).image || serverThumb(url) : undefined }) : h('img', { class: 'mp-media', 'data-src': quick, 'data-full': url, alt: '', draggable: 'false', decoding: 'async', onError: (e) => { const m = e.currentTarget; @@ -12562,7 +12594,7 @@ export function messagesFeature(ctx) { m.closest('.mp-slide').classList.add('mp-gone'); m.insertAdjacentElement('afterend', h('div', { class: 'mp-missing' }, h('div', { class: 'mp-missing-ico' }, '🖼'), h('div', {}, t('mediaPicGone')), host ? h('div', { class: 'small muted' }, host) : null)); } }); - const caption = mediaCaption(ev); + const caption = mediaCaption(ev, true); const stop = (fn) => (e) => { e.stopPropagation(); fn(e); }; const slide = h('div', { class: 'mp-slide', 'data-k': mediaKey(it) }, media, @@ -12570,7 +12602,7 @@ export function messagesFeature(ctx) { h('div', { class: 'mp-left' }, h('button', { class: 'mp-author', type: 'button', onClick: stop(() => { leavePager(); openProfile(ev.pubkey); }) }, avatar(ev.pubkey, 'chat-avatar mini', false), h('span', {}, displayName(ev.pubkey))), - caption ? h('div', { class: 'mp-caption', onClick: stop((e) => e.currentTarget.classList.toggle('open')) }, caption) : null), + caption ? captionNode(ev, caption) : null), h('div', { class: 'mp-counts' }, // as in the feed: a tap picks how you feel about it (the emoji // sheet, over the pager), a tap again takes it back @@ -12777,12 +12809,12 @@ export function messagesFeature(ctx) { } const headStarted = new Set(); function headStart(url, share = 1) { - if (!url || url.startsWith('blob:') || headStarted.has(url) || frugalData()) return; + if (!url || url.startsWith('blob:') || headStarted.has(url) || frugalData()) return Promise.resolve(); headStarted.add(url); if (headStarted.size > 300) headStarted.delete(headStarted.values().next().value); const want = Math.max(HEAD_MIN, Math.round(headStartBytes(url) * share)); const t0 = performance.now(); - fetch(url, { headers: { Range: 'bytes=0-' + (want - 1) } }).then(async (r) => { + return fetch(url, { headers: { Range: 'bytes=0-' + (want - 1) } }).then(async (r) => { const reader = r.ok && r.body ? r.body.getReader() : null; if (!reader) return; let got = 0; @@ -12809,11 +12841,11 @@ export function messagesFeature(ctx) { // first frame itself (only the start of the file, when it leads with // its index or is served in ranges) — again on every return, since // a slide left behind lets go of its file - slidePoster(slide); + slidePoster(slide, true); if (videoThumbs.has(url)) pagerRelease(slide); else grabFrame(url).then((kept) => { if (!pager || !slide.isConnected) return; - if (kept) { slidePoster(slide); return; } + if (kept) { slidePoster(slide, true); return; } // no frame to keep (the host's bytes can't be read, or the index // trails): the player shows its own first frame under the cover const d = videoInfo.get(url) || {}; @@ -12835,8 +12867,10 @@ export function messagesFeature(ctx) { }); } // the kept frame as the poster, looked up as the slide comes near (it may - // have been kept after the slide was built) - function slidePoster(slide) { + // have been kept after the slide was built) — only where a poster is + // wanted: a careful connection, or a heavy clip under its cover (force) + function slidePoster(slide, force = false) { + if (!force && !frugalData()) return; const v = slide.querySelector('video.mp-media'); const thumb = v && videoThumbs.get(v.getAttribute('data-src')); if (thumb && v.getAttribute('poster') !== thumb) v.setAttribute('poster', thumb); |
