diff options
| author | Adam Soltys <[email protected]> | 2026-10-13 05:19:47 +0000 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-13 05:19:47 +0000 |
| commit | 6488a0a687144e217f71ab1c7a768613b0f17e58 (patch) | |
| tree | 3bff8ea8f4f350bb1bfa4793ac1fc94ef95cca5c /src/features | |
| parent | 6fdca50f17b1e814bbf6ba1fc1ffcb18001c54fd (diff) | |
| download | coinosv3-6488a0a687144e217f71ab1c7a768613b0f17e58.tar.gz coinosv3-6488a0a687144e217f71ab1c7a768613b0f17e58.zip | |
Media pager: past the last slide, a "fetching the next video" screen — and the next one, when it comes, is moved onto. Adam reached the last clip before the next was in, and a swipe only nudged the clip and sprang back. Now the pager ends in a screen of its own: a ring sweeping round, three sparks orbiting the other way, the play mark (a picture mark in a pictures pager) breathing in the middle, and "Fetching the next video…" (or "Fetching more pictures…") with a sheen running through it, all still under prefers-reduced-motion. Landing there stops the clip left behind and asks for more at once (the feed pages on; the media grid's pager crawls); slides that arrive go in above it, and one arriving while the reader waits is moved onto and plays. With nothing older to be had (the feed at its end, or the media index), it says "That's everything for now" and goes still. The scroller no longer anchors (overflow-anchor: none), so a slide put in above the screen doesn't hold the screen in view. Strings in all 20 languages. tools/pager-tail-test.js (relays that take 3 s for anything older: the screen at the end, the clip behind stopped, the next clip moved onto and playing, the end saying that's everything, Escape onto the last clip's post); feed-pager-ahead and feed-media-views count slides without it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KPw21NEpWeTGu6swVASmBt
Diffstat (limited to 'src/features')
| -rw-r--r-- | src/features/messages.js | 58 |
1 files changed, 51 insertions, 7 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index 61cdc18..6e977af 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -11994,7 +11994,7 @@ export function messagesFeature(ctx) { function dropPager() { const p = pager; pager = null; - try { p.io.disconnect(); p.near.disconnect(); } catch {} + try { p.io.disconnect(); p.near.disconnect(); p.tailIo.disconnect(); } catch {} clearInterval(p.tick); cancelAnimationFrame(p.raf); window.removeEventListener('resize', p.onResize); @@ -12114,6 +12114,8 @@ export function messagesFeature(ctx) { }; return bar; } + // the pager's slides, without the loading one at the end + const pagerSlides = () => (pager ? [...pager.scroller.children].filter((x) => !x._tail) : []); function pagerAttach(slide) { 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')); @@ -12136,7 +12138,7 @@ export function messagesFeature(ctx) { // clip holds nothing. function pagerNeighbours(slide) { if (!pager || pager.kind !== 'videos') return; - const slides = [...pager.scroller.children]; + const slides = pagerSlides(); const i = slides.indexOf(slide); const eager = !frugalData(); slides.forEach((s, j) => { @@ -12265,7 +12267,7 @@ export function messagesFeature(ctx) { h('span', { class: 'on' }, '\u{1F50A}'), h('span', { class: 'off' }, '\u{1F507} ' + t('videoUnmute'))) : null)); // a tap shows (or hides) who posted it and what they said el.addEventListener('click', (e) => { - if (e.target.closest('button, a, .mp-caption, .mp-seek')) return; + if (e.target.closest('button, a, .mp-caption, .mp-seek, .mp-tail')) return; if (Date.now() - videoTapAt < 800) return; // the click of the touch that opened it if (pager && pager.fromFeed && video) { // A clip opened from the feed: a tap pauses it and brings up who @@ -12291,6 +12293,32 @@ export function messagesFeature(ctx) { if (el.classList.contains('show')) pagerCounts(pager && pager.cur); }); pager = { el, scroller, kind: want.kind, keys: new Set(), cur: null, feed: want.feed, fromFeed: !!want.fromFeed, start: want.start, at: want.at || 0 }; + // The end of the pager: swiping past the last slide lands here, on + // something saying more is coming (not a nudge that goes nowhere) — and + // asks for it at once. Slides that arrive go in above it; one arriving + // while the reader waits here is moved onto, and plays. With nothing + // more to be had, it says so. + const tail = h('div', { class: 'mp-tail', 'aria-live': 'polite' }, + h('div', { class: 'mp-loader', 'aria-hidden': 'true', html: video + ? '<span class="mp-orbit"><i></i><i></i><i></i></span><span class="mp-core"><svg viewBox="0 0 24 24" width="30" height="30"><path d="M8 5.5v13l10.5-6.5z" fill="currentColor"/></svg></span>' + : '<span class="mp-orbit"><i></i><i></i><i></i></span><span class="mp-core"><svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"><rect x="3.5" y="4.5" width="17" height="15" rx="2.5"/><path d="M3.5 16l5-5 4 4 2.5-2.5 5 5"/><circle cx="15.5" cy="9" r="1.6" fill="currentColor" stroke="none"/></svg></span>' }), + h('div', { class: 'mp-tail-text' }, t(video ? 'pagerFetchingVideo' : 'pagerFetchingPictures'))); + tail._tail = true; + scroller.append(tail); + pager.tail = tail; + pager.tailIo = new IntersectionObserver((entries) => { + for (const e of entries) { + if (!pager) return; + pager.onTail = e.isIntersecting && e.intersectionRatio >= 0.6; + if (!pager.onTail) continue; + const v = pager.cur && pager.cur.querySelector('video.mp-media'); + if (v) v.pause(); + el.classList.remove('show'); + if (pager.fromFeed) pagerFeedAhead(); else crawlMedia().catch(() => {}); + pagerTailState(); + } + }, { root: scroller, threshold: [0, 0.6] }); + pager.tailIo.observe(tail); pager.near = new IntersectionObserver((entries) => { for (const e of entries) { if (!e.isIntersecting) continue; @@ -12313,13 +12341,13 @@ export function messagesFeature(ctx) { pagerCounts(e.target); // near the end: ask the feed for more, the pager picks them up if (pager.fromFeed) pagerFeedAhead(); - else { const slides = scroller.children; if ([...slides].indexOf(e.target) >= slides.length - 3) crawlMedia().catch(() => {}); } + else { const slides = pagerSlides(); if (slides.indexOf(e.target) >= slides.length - 3) crawlMedia().catch(() => {}); } } }, { root: scroller, threshold: [0.6] }); growPager(); document.body.append(el); document.documentElement.classList.add('no-scroll'); - const first = [...scroller.children].find((s) => s.getAttribute('data-k') === want.start) || scroller.firstChild; + const first = pagerSlides().find((s) => s.getAttribute('data-k') === want.start) || scroller.firstChild; if (first) scroller.scrollTop = first.offsetTop; // the clip on screen's seek bar, every frame (timeupdate comes a few times a second) if (video) { @@ -12349,7 +12377,7 @@ export function messagesFeature(ctx) { try { const c = feedNow(); for (let pages = 0; pages < 8 && pager && pager.fromFeed; pages++) { - const slides = [...pager.scroller.children]; + const slides = pagerSlides(); if (slides.length - 1 - slides.indexOf(pager.cur) > (PAGER_AHEAD[pager.kind] || 4)) break; const shown = c.shown, held = c.notes.length; await loadOlderFeed(); @@ -12363,15 +12391,31 @@ export function messagesFeature(ctx) { function growPager() { if (!pager) return; const c = feedNow(); + const waiting = pager.onTail; + let first = null; for (const it of (pager.fromFeed ? feedMediaItems(c, pager.kind) : mediaItems(c, pager.kind))) { const k = mediaKey(it); if (pager.keys.has(k)) continue; pager.keys.add(k); const s = pagerSlide(it); - pager.scroller.append(s); + pager.scroller.insertBefore(s, pager.tail || null); + first ||= s; pager.near.observe(s); pager.io.observe(s); } + // the reader was waiting at the end: onto the first that came + if (waiting && first) { pager.onTail = false; pager.scroller.scrollTop = first.offsetTop; } + pagerTailState(); + } + // the end says "more coming", or — the feed (or the media index) has no + // more to give — that this is everything + function pagerTailState() { + if (!pager || !pager.tail) return; + const c = feedNow(); + const done = pager.fromFeed ? !!c.end && c.shown >= c.notes.length && !c.loadingMore : !!mediaIndex(c.id).end; + if (pager.tail.classList.contains('done') === done) return; + pager.tail.classList.toggle('done', done); + pager.tail.querySelector('.mp-tail-text').textContent = done ? t('pagerCaughtUp') : t(pager.kind === 'videos' ? 'pagerFetchingVideo' : 'pagerFetchingPictures'); } if (typeof window !== 'undefined') { window.addEventListener('keydown', (e) => { |
