aboutsummaryrefslogtreecommitdiffstats
path: root/src/features
diff options
context:
space:
mode:
Diffstat (limited to 'src/features')
-rw-r--r--src/features/messages.js104
1 files changed, 81 insertions, 23 deletions
diff --git a/src/features/messages.js b/src/features/messages.js
index a3c7cd5..6548e99 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -12225,8 +12225,33 @@ 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) : []);
+ // The pager is posts stacked top to bottom (up and down go from post to
+ // post), each a row of its pictures or clips — its quotes' too — side by
+ // side (left and right go within it). Its slides, in reading order:
+ const pagerSlides = () => (pager ? [...pager.scroller.querySelectorAll('.mp-slide')] : []);
+ const pagerGroups = () => (pager ? [...pager.scroller.querySelectorAll(':scope > .mp-group')] : []);
+ // the slide a post's row rests on
+ const rowSlide = (g) => { const row = g && g._row; if (!row || !row.children.length) return null; return row.children[Math.min(row.children.length - 1, Math.round(row.scrollLeft / (row.clientWidth || 1)))]; };
+ // bring a slide to the screen: its post, and within it, the slide
+ function pagerShow(slide) {
+ const g = slide && slide.closest('.mp-group');
+ if (!g) return;
+ pager.scroller.scrollTop = g.offsetTop;
+ g._row.scrollLeft = slide.offsetLeft;
+ }
+ // what's next from a slide: down (the next post, where its row rests),
+ // along (the next in this post), and the post after next
+ function pagerAhead(slide) {
+ const out = [];
+ const nextGroup = (g) => { let n = g && g.nextElementSibling; while (n && !n.classList.contains('mp-group')) n = n.nextElementSibling; return n; };
+ const g1 = nextGroup(slide.closest('.mp-group')), g2 = nextGroup(g1);
+ const down = g1 && rowSlide(g1);
+ if (down) out.push(down);
+ if (slide.nextElementSibling) out.push(slide.nextElementSibling);
+ const down2 = g2 && rowSlide(g2);
+ if (down2) out.push(down2);
+ return out;
+ }
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'));
@@ -12252,19 +12277,21 @@ export function messagesFeature(ctx) {
const slides = pagerSlides();
const i = slides.indexOf(slide);
const eager = !frugalData();
+ const ahead = pagerAhead(slide);
slides.forEach((s, j) => {
- if (Math.abs(j - i) <= 2) slidePoster(s);
+ const k = ahead.indexOf(s);
+ if (Math.abs(j - i) <= 2 || k >= 0) slidePoster(s);
if (j === i) return;
const v = s.querySelector('video.mp-media');
- if (eager && (j === i + 1 || j === i + 2)) {
+ if (eager && k >= 0) {
pagerRelease(s);
- headStart(playSrc(v.getAttribute('data-src')), j === i + 1 ? 1 : 0.5);
+ headStart(playSrc(v.getAttribute('data-src')), k === 0 ? 1 : 0.5);
return;
}
- if (j === i + 1) {
+ if (k === 0) {
const url = v.getAttribute('data-src');
probeVideo(url).then(() => {
- if (!pager || pager.cur === s || videoHeavy(url) || slides.indexOf(pager.cur) !== i) return;
+ if (!pager || pager.cur === s || videoHeavy(url) || pager.cur !== slide) return;
v.preload = 'metadata';
pagerAttach(s);
});
@@ -12468,7 +12495,10 @@ export function messagesFeature(ctx) {
for (const e of entries) {
if (!e.isIntersecting) continue;
watchZaps([e.target._ev.id]);
- if (pager.kind === 'images') pagerAttach(e.target); // pictures are cheap; clips are pagerNeighbours' call
+ // pictures are cheap — a post's all at once, the ones along its row
+ // too (out of sight sideways, they'd load only on arrival); clips
+ // are pagerNeighbours' call
+ if (pager.kind === 'images') for (const x of e.target.closest('.mp-group').querySelectorAll('.mp-slide')) pagerAttach(x);
}
}, { root: scroller, rootMargin: '150% 0px' });
pager.io = new IntersectionObserver((entries) => {
@@ -12489,14 +12519,14 @@ 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 = pagerSlides(); if (slides.indexOf(e.target) >= slides.length - 3) crawlMedia().catch(() => {}); }
+ else { const gs = pagerGroups(); if (gs.indexOf(e.target.closest('.mp-group')) >= gs.length - 3) crawlMedia().catch(() => {}); }
}
}, { root: scroller, threshold: [0.6] });
growPager();
document.body.append(el);
document.documentElement.classList.add('no-scroll');
- const first = pagerSlides().find((s) => s.getAttribute('data-k') === want.start) || scroller.firstChild;
- if (first) scroller.scrollTop = first.offsetTop;
+ const first = pagerSlides().find((s) => s.getAttribute('data-k') === want.start) || pagerSlides()[0];
+ if (first) pagerShow(first);
// the clip on screen's seek bar, every frame (timeupdate comes a few times a second)
if (video) {
const p = pager;
@@ -12504,18 +12534,18 @@ export function messagesFeature(ctx) {
p.raf = requestAnimationFrame(frame);
}
// turned on its side (or back), the slides change height: stay on this one
- pager.onResize = () => { if (pager && pager.cur) pager.scroller.scrollTop = pager.cur.offsetTop; };
+ pager.onResize = () => { if (pager && pager.cur) pagerShow(pager.cur); };
window.addEventListener('resize', pager.onResize);
// new posts land in the feed while the pager is open; counts move
pager.tick = setInterval(() => { if (pager) { growPager(); pagerFeedAhead(); if (el.classList.contains('show')) pagerCounts(pager.cur); } }, 1500);
}
// A pager opened from the feed walks the feed's own posts, which page in
// ten at a time as the feed is scrolled. Swiping is the scrolling here:
- // with only a few slides left past the one on screen (PAGER_AHEAD), the feed pages on
+ // with only a few posts left below the one on screen (PAGER_AHEAD), the feed pages on
// (the posts it holds, then older ones from the relays) until there are
// more — several pages if those posts carry none. A page that brings
// nothing (the feed busy, or at its end) stops it; the tick tries again.
- // how many slides ahead to keep: clips are fewer and further between
+ // how many posts ahead to keep: clips are fewer and further between
// (more pages to find the next) and each is watched a while, so further
const PAGER_AHEAD = { images: 4, videos: 6 };
let pagerFeeding = false;
@@ -12525,8 +12555,8 @@ export function messagesFeature(ctx) {
try {
const c = feedNow();
for (let pages = 0; pages < 8 && pager && pager.fromFeed; pages++) {
- const slides = pagerSlides();
- if (slides.length - 1 - slides.indexOf(pager.cur) > (PAGER_AHEAD[pager.kind] || 4)) break;
+ const gs = pagerGroups();
+ if (gs.length - 1 - gs.indexOf(pager.cur && pager.cur.closest('.mp-group')) > (PAGER_AHEAD[pager.kind] || 4)) break;
const shown = c.shown, held = c.notes.length;
await loadOlderFeed();
growPager();
@@ -12534,27 +12564,53 @@ export function messagesFeature(ctx) {
}
} catch {} finally { pagerFeeding = false; }
}
- // Slides for feed items the pager doesn't have yet, appended (never
+ // Slides for feed items the pager doesn't have yet: into their post's row
+ // (a quote fetched late joins its post), a new post appended below (never
// inserted above the one on screen).
function growPager() {
if (!pager) return;
const c = feedNow();
const waiting = pager.onTail;
let first = null;
+ pager.groups ||= new Map();
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.insertBefore(s, pager.tail || null);
- first ||= s;
+ const gk = it.row ? it.row.id : it.ev.id;
+ let g = pager.groups.get(gk);
+ if (!g) {
+ g = pagerGroup();
+ pager.groups.set(gk, g);
+ pager.scroller.insertBefore(g, pager.tail || null);
+ first ||= s;
+ }
+ 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; pager.scroller.scrollTop = first.offsetTop; }
+ if (waiting && first) { pager.onTail = false; pagerShow(first); }
pagerTailState();
}
+ // A post in the pager: its row of slides, side by side, and — with more
+ // than one — dots saying how many and which.
+ function pagerGroup() {
+ const row = h('div', { class: 'mp-row' });
+ const dots = h('div', { class: 'mp-dots', 'aria-hidden': 'true' });
+ const g = h('div', { class: 'mp-group' }, row, dots);
+ g._row = row; g._dots = dots;
+ row.addEventListener('scroll', () => pagerDots(g), { passive: true });
+ return g;
+ }
+ function pagerDots(g) {
+ const n = g._row.children.length;
+ if (g._dots.children.length !== (n > 1 ? n : 0)) g._dots.replaceChildren(...(n > 1 ? Array.from({ length: n }, () => h('i')) : []));
+ 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() {
@@ -12569,9 +12625,11 @@ export function messagesFeature(ctx) {
window.addEventListener('keydown', (e) => {
if (!pager || e.defaultPrevented) return;
if (e.key === 'Escape') { e.preventDefault(); dismissPager(); return; }
- // the arrows, any of the four: next (down, right) or previous (up, left)
- const step = ['ArrowDown', 'ArrowRight', 'j', 'PageDown'].includes(e.key) ? 1 : ['ArrowUp', 'ArrowLeft', 'k', 'PageUp'].includes(e.key) ? -1 : 0;
- if (step) { e.preventDefault(); pager.scroller.scrollBy({ top: step * pager.scroller.clientHeight, behavior: 'smooth' }); return; }
+ // up and down: the next post, or the one before; left and right: along this post's row
+ const v = ['ArrowDown', 'j', 'PageDown'].includes(e.key) ? 1 : ['ArrowUp', 'k', 'PageUp'].includes(e.key) ? -1 : 0;
+ const hz = ['ArrowRight', 'l'].includes(e.key) ? 1 : ['ArrowLeft', 'h'].includes(e.key) ? -1 : 0;
+ if (v) { e.preventDefault(); pager.scroller.scrollBy({ top: v * pager.scroller.clientHeight, behavior: 'smooth' }); return; }
+ if (hz) { e.preventDefault(); const row = pager.onTail ? null : pager.cur && pager.cur.parentElement; if (row) row.scrollBy({ left: hz * row.clientWidth, behavior: 'smooth' }); return; }
if (e.key === ' ' && pager.cur) {
const v = pager.cur.querySelector('video.mp-media');
if (v) { e.preventDefault(); if (v.paused) v.play().catch(() => {}); else v.pause(); }