aboutsummaryrefslogtreecommitdiffstats
path: root/src/features
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-14 23:21:55 -0700
committerAdam Soltys <[email protected]>2026-10-14 23:21:55 -0700
commit686309a584a8e7a13091cb194e42aa09e2aec5d5 (patch)
treee1e934385b262e5162686fa69743774dda099937 /src/features
parent63d43428468bb5dc336006bc345769b071905204 (diff)
downloadcoinosv3-686309a584a8e7a13091cb194e42aa09e2aec5d5.tar.gz
coinosv3-686309a584a8e7a13091cb194e42aa09e2aec5d5.zip
Loop media pager through loaded posts and show position
Diffstat (limited to 'src/features')
-rw-r--r--src/features/messages.js100
1 files changed, 44 insertions, 56 deletions
diff --git a/src/features/messages.js b/src/features/messages.js
index 6d3aefa..ccba6ad 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -12142,7 +12142,7 @@ export function messagesFeature(ctx) {
function dropPager() {
const p = pager;
pager = null;
- try { p.io.disconnect(); p.near.disconnect(); p.tailIo.disconnect(); } catch {}
+ try { p.io.disconnect(); p.near.disconnect(); } catch {}
clearInterval(p.tick);
cancelAnimationFrame(p.raf);
window.removeEventListener('resize', p.onResize);
@@ -12513,13 +12513,18 @@ export function messagesFeature(ctx) {
if (!pager) return false;
const here = pagerHere();
if (v) {
- const from = pager.onTail || !here ? pager.tail : here.closest('.mp-group');
- const to = from && (v > 0 ? from.nextElementSibling : from.previousElementSibling);
- if (to) pagerGlide(pager.scroller, 'y', pagerAt(pager.scroller, to, true));
+ const groups = pagerGroups();
+ const from = here && here.closest('.mp-group');
+ const index = groups.indexOf(from) + v;
+ const to = groups[(index + groups.length) % groups.length];
+ if (to) {
+ if (index < 0 || index >= groups.length) pagerWrap(to);
+ else pagerGlide(pager.scroller, 'y', pagerAt(pager.scroller, to, true));
+ }
return !!to;
}
if (hz) {
- const to = !pager.onTail && here && (hz > 0 ? here.nextElementSibling : here.previousElementSibling);
+ const to = here && (hz > 0 ? here.nextElementSibling : here.previousElementSibling);
if (to) pagerGlide(here.parentElement, 'x', pagerAt(here.parentElement, to, false));
return !!to;
}
@@ -12536,7 +12541,8 @@ export function messagesFeature(ctx) {
let fly = null; // { to: the post flown to, n: notches in this run, at, dir }
function pagerFly(dir, now) {
if (!pager) return;
- const stops = [...pager.scroller.children]; // the posts, then the tail
+ const stops = pagerGroups();
+ if (!stops.length) return;
const live = fly && now - fly.at < 400 && pager.scroller._glide;
let from, n = 1;
if (live && fly.dir === dir) { from = fly.to; n = fly.n + 1; }
@@ -12545,9 +12551,11 @@ export function messagesFeature(ctx) {
from = stops.reduce((b, g) => (Math.abs(g.offsetTop - y) < Math.abs(b.offsetTop - y) ? g : b));
} else {
const here = pagerHere();
- from = pager.onTail || !here ? pager.tail : here.closest('.mp-group');
+ from = here && here.closest('.mp-group');
}
- const i = Math.max(0, Math.min(stops.length - 1, stops.indexOf(from) + dir * (n <= 2 ? 1 : 2)));
+ const next = stops.indexOf(from) + dir * (n <= 2 ? 1 : 2);
+ const i = (next % stops.length + stops.length) % stops.length;
+ if (next < 0 || next >= stops.length) { fly = null; pagerWrap(stops[i]); return; }
const to = stops[i];
fly = { to, n, at: now, dir };
if (live) pager.scroller._onDone = null; // the flight goes on: not a landing
@@ -12567,11 +12575,30 @@ export function messagesFeature(ctx) {
pager.el.classList.toggle('has-right', !!slide.nextElementSibling);
}
+ // Crossing the boundary jumps directly to the other end, without flying
+ // backwards through every loaded post or waiting for a network request.
+ function pagerWrap(group) {
+ if (!pager || !group) return;
+ pager.scroller._onDone = null;
+ pagerHold(pager.scroller);
+ pager.scroller.style.scrollSnapType = '';
+ pager.flying = false;
+ const slide = rowSlide(group);
+ if (slide) { pagerShow(slide); pagerArrive(slide); }
+ }
+ function pagerPosition() {
+ if (!pager) return;
+ const slides = pagerSlides();
+ const index = slides.indexOf(pager.cur || pager.anchor);
+ pager.el.querySelector('.mp-position').textContent = (index < 0 ? 0 : index + 1) + ' / ' + slides.length;
+ }
+
function buildPager(want) {
const video = want.kind === 'videos';
const scroller = h('div', { class: 'mp-scroll' });
const el = h('div', { class: 'media-pager' + (video ? ' videos' : '') + (pagerLoud ? '' : ' quiet'), role: 'dialog', 'aria-modal': 'true' },
scroller,
+ h('div', { class: 'mp-position', role: 'status' }, '0 / 0'),
h('div', { class: 'mp-top' },
h('button', { class: 'mp-x', type: 'button', 'aria-label': t('close'), onClick: (e) => { e.stopPropagation(); dismissPager(); } }, '×'),
video ? h('button', { class: 'mp-sound', type: 'button', onClick: (e) => { e.stopPropagation(); pagerSound(!pagerLoud); } },
@@ -12662,11 +12689,13 @@ export function messagesFeature(ctx) {
// let go of a drag: on to the next (or the one before), or back
function settle(d, step, vel = 0) {
const box = d.box, y = d.axis === 'y';
- const stops = [...box.children].filter((c) => c.matches(y ? '.mp-group, .mp-tail' : '.mp-slide')).map((c) => pagerAt(box, c, y));
+ const stops = [...box.children].filter((c) => c.matches(y ? '.mp-group' : '.mp-slide')).map((c) => pagerAt(box, c, y));
if (!stops.length) return;
// the stop it set off from, then one along (or back)
const from = stops.reduce((bi, p, i) => (Math.abs(p - d.start) < Math.abs(stops[bi] - d.start) ? i : bi), 0);
- pagerGlide(box, d.axis, stops[Math.min(stops.length - 1, Math.max(0, from + step))], vel);
+ const next = from + step;
+ if (y && (next < 0 || next >= stops.length)) { pagerWrap(pagerGroups()[(next + stops.length) % stops.length]); return; }
+ pagerGlide(box, d.axis, stops[Math.min(stops.length - 1, Math.max(0, next))], vel);
}
el.addEventListener('touchend', (e) => {
const d = touch;
@@ -12700,7 +12729,7 @@ export function messagesFeature(ctx) {
});
// a tap shows (or hides) who posted it and what they said
function pagerTap(target) {
- if (target.closest('button, a, .mp-caption, .mp-seek, .mp-tail')) return;
+ if (target.closest('button, a, .mp-caption, .mp-seek')) return;
if (Date.now() - videoTapAt < 800) return; // the click of the touch that opened it
if (pager && video) {
// A clip (from the feed or the media grid): a tap anywhere but the
@@ -12727,32 +12756,6 @@ 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, profile: want.profile || null, 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 (pagerSource()) 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;
@@ -12776,6 +12779,7 @@ export function messagesFeature(ctx) {
pager.anchor = slide;
pager.curAt = Date.now();
pagerArrows(slide);
+ pagerPosition();
if (pager.fromFeed) { const row = feedNow().notes.find((x) => x.id === slide._rowId); markSeen(row || slide._ev); }
else if (pager.profile) markSeen(slide._ev);
playSlide(slide);
@@ -12818,8 +12822,7 @@ export function messagesFeature(ctx) {
pager.onResize = () => {
if (!pager) return;
pager.screenH = pager.scroller.clientHeight;
- if (pager.onTail) pager.scroller.scrollTop = pager.tail.offsetTop;
- else if (pager.anchor) pagerShow(pager.anchor);
+ if (pager.anchor) pagerShow(pager.anchor);
};
window.addEventListener('resize', pager.onResize);
// new posts land in the feed while the pager is open; counts move
@@ -12857,8 +12860,6 @@ export function messagesFeature(ctx) {
function growPager() {
if (!pager) return;
const c = feedNow();
- const waiting = pager.onTail;
- let first = null;
pager.groups ||= new Map();
const src = pagerSource();
for (const it of (src ? src.items(pager.kind) : mediaItems(c, pager.kind))) {
@@ -12871,17 +12872,14 @@ export function messagesFeature(ctx) {
if (!g) {
g = pagerGroup();
pager.groups.set(gk, g);
- pager.scroller.insertBefore(g, pager.tail || null);
- first ||= s;
+ pager.scroller.append(g);
}
g._row.append(s);
pagerDots(g);
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; pagerShow(first); }
- pagerTailState();
+ pagerPosition();
}
// A post in the pager: its row of slides, side by side, and — with more
// than one — dots saying how many and which.
@@ -12899,16 +12897,6 @@ export function messagesFeature(ctx) {
const at = rowSlide(g);
[...g._dots.children].forEach((d, i) => d.classList.toggle('on', g._row.children[i] === at));
}
- // 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 src = pagerSource();
- const done = src ? src.done() : !!mediaIndex(feedNow().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) => {
if (!pager || e.defaultPrevented) return;