aboutsummaryrefslogtreecommitdiffstats
path: root/src/features
diff options
context:
space:
mode:
Diffstat (limited to 'src/features')
-rw-r--r--src/features/messages.js56
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);