diff options
Diffstat (limited to 'src/features/messages.js')
| -rw-r--r-- | src/features/messages.js | 38 |
1 files changed, 37 insertions, 1 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index a6f49bb..20cfaa6 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -12267,10 +12267,27 @@ export function messagesFeature(ctx) { function pagerSlide(it) { const { ev, url } = it; const video = pager.kind === 'videos'; + // A picture flies in as the feed's copy of it (1100px wide, decoded + // already if the feed painted it; else the server's, cached at the CDN) + // and is sharpened to the original once it comes to rest (pagerSharpen). + // Originals as they passed — camera files, several megapixels each — + // decoded on the main thread and were uploaded as textures mid-flight, + // 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; 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) }) - : h('img', { class: 'mp-media', 'data-src': url, alt: '', draggable: 'false' }); + : h('img', { class: 'mp-media', 'data-src': quick, 'data-full': url, alt: '', draggable: 'false', decoding: 'async', + onError: (e) => { + const m = e.currentTarget; + if ((m.getAttribute('src') || '') !== url) { m.setAttribute('data-src', url); m.setAttribute('src', url); return; } + // the original is gone too (its host down, the file deleted): say + // so, in place of the browser's broken-picture glyph + let host = ''; try { host = new URL(url).hostname; } catch {} + 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 stop = (fn) => (e) => { e.stopPropagation(); fn(e); }; const slide = h('div', { class: 'mp-slide', 'data-k': mediaKey(it) }, @@ -12393,6 +12410,22 @@ export function messagesFeature(ctx) { const m = slide.querySelector('.mp-media'); if (m && !m.getAttribute('src')) m.setAttribute('src', m.tagName === 'VIDEO' ? playSrc(m.getAttribute('data-src')) : m.getAttribute('data-src')); } + // The slide at rest gets its picture's original — fetched and decoded + // aside first, then swapped in (same box, same aspect: nothing moves) — + // a moment after landing, so a quick page past it costs nothing. + function pagerSharpen(slide) { + const m = slide && slide.querySelector('img.mp-media'); + const full = m && m.getAttribute('data-full'); + if (!full || m.getAttribute('src') === full || m._sharp) return; + m._sharp = setTimeout(() => { + m._sharp = 0; + if (!pager || pager.cur !== slide) return; + const im = new Image(); + im.decoding = 'async'; + im.onload = () => { const go = () => { if (pager && m.isConnected && m.getAttribute('src') !== full) { m.setAttribute('data-src', full); m.setAttribute('src', full); } }; (im.decode ? im.decode().then(go, go) : go()); }; + im.src = full; + }, 300); + } function pagerRelease(slide) { const m = slide.querySelector('video.mp-media'); if (m && m.getAttribute('src')) { try { m.pause(); m.removeAttribute('src'); m.load(); } catch {} } @@ -12908,9 +12941,12 @@ export function messagesFeature(ctx) { if (v && pager.curAt) { const secs = (Date.now() - pager.curAt) / 1000; dwell = dwell ? dwell * 0.7 + secs * 0.3 : secs; } if (pagerSource() && video) el.classList.remove('show'); } + if (pager.cur) pager.cur.classList.remove('mp-cur'); + slide.classList.add('mp-cur'); // (the one with the layer for a pinch: style.css) pager.cur = slide; pager.anchor = slide; pager.curAt = Date.now(); + pagerSharpen(slide); pagerArrows(slide); pagerPosition(); if (pager.fromFeed) { const row = feedNow().notes.find((x) => x.id === slide._rowId); markSeen(row || slide._ev); } |
