From 5550e929641b282a3de2c2cda66782d7c58689a1 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Tue, 13 Oct 2026 18:24:57 +0000 Subject: Pager: a snappy glide to the next slide, not the browser's slow smooth scroll Letting go of a swipe (or an arrow key) runs the pager's own eased-out glide, at most 220ms and quicker after a fast flick; a finger catching it mid-glide takes it from there. A swipe now lands in ~240ms (was ~425ms). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KPw21NEpWeTGu6swVASmBt --- src/features/messages.js | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) (limited to 'src/features') diff --git a/src/features/messages.js b/src/features/messages.js index 3cb94fd..a7dc54c 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -12433,6 +12433,26 @@ export function messagesFeature(ctx) { const cur = pager.cur && pager.cur.querySelector('video.mp-media'); if (cur && cur.paused) cur.play().catch(() => {}); } + // The pager's own glide from slide to slide: the browser's smooth scroll + // dawdles (half a second and more on Android). Quick and eased out, faster + // still after a fast flick; snapping stays off until it lands. + function pagerGlide(box, axis, to, vel = 0) { + const y = axis === 'y', from = y ? box.scrollTop : box.scrollLeft, dist = to - from; + cancelAnimationFrame(box._glide); + box.style.scrollSnapType = 'none'; + const done = () => { box._glide = 0; box.style.scrollSnapType = ''; }; + if (Math.abs(dist) < 1) { done(); return; } + const ms = Math.max(90, Math.min(220, Math.abs(dist) / Math.max(2.5, Math.abs(vel) * 1.5))); + const t0 = performance.now(); + const step = (now) => { + const k = Math.min(1, (now - t0) / ms), e = 1 - Math.pow(1 - k, 3); + if (y) box.scrollTop = from + dist * e; else box.scrollLeft = from + dist * e; + if (k < 1) box._glide = requestAnimationFrame(step); else done(); + }; + box._glide = requestAnimationFrame(step); + } + const pagerHold = (box) => { if (box && box._glide) { cancelAnimationFrame(box._glide); box._glide = 0; } }; + function buildPager(want) { const video = want.kind === 'videos'; const scroller = h('div', { class: 'mp-scroll' }); @@ -12470,6 +12490,7 @@ export function messagesFeature(ctx) { d.axis = Math.abs(dx) > Math.abs(dy) ? 'x' : 'y'; d.box = d.axis === 'y' ? pager.scroller : d.slide && d.slide.parentElement; if (!d.box) { touch = null; return; } + pagerHold(d.box); // caught mid-glide: from where it is d.start = d.axis === 'y' ? d.box.scrollTop : d.box.scrollLeft; d.box.style.scrollSnapType = 'none'; // following the finger: no snapping under it d.track = []; @@ -12480,14 +12501,12 @@ export function messagesFeature(ctx) { if (d.track.length > 5) d.track.shift(); }, { passive: false }); // let go of a drag: on to the next (or the one before), or back - function settle(d, step) { + function settle(d, step, vel = 0) { const box = d.box, y = d.axis === 'y'; const size = (y ? box.clientHeight : box.clientWidth) || 1; const last = Math.max(0, Math.round(((y ? box.scrollHeight : box.scrollWidth) - size) / size)); const to = Math.min(last, Math.max(0, Math.round(d.start / size) + step)); - box.scrollTo({ [y ? 'top' : 'left']: to * size, behavior: 'smooth' }); - clearTimeout(box._snapBack); - box._snapBack = setTimeout(() => { box.style.scrollSnapType = ''; }, 450); + pagerGlide(box, d.axis, to * size, vel); } el.addEventListener('touchend', (e) => { const d = touch; @@ -12500,7 +12519,7 @@ export function messagesFeature(ctx) { const a = d.track[0], b = d.track[d.track.length - 1]; const vel = a && b && b.t > a.t ? (b.v - a.v) / (b.t - a.t) : 0; const go = Math.abs(delta) > size * SWIPE_FRAC || (Math.abs(delta) >= FLICK_MIN && Math.abs(vel) > FLICK && Math.sign(vel) === Math.sign(delta)); - settle(d, go ? (delta < 0 ? 1 : -1) : 0); + settle(d, go ? (delta < 0 ? 1 : -1) : 0, go ? vel : 0); return; } if (Date.now() - d.at > 500) return; // a press, not a tap @@ -12715,13 +12734,13 @@ export function messagesFeature(ctx) { e.preventDefault(); const from = pager.onTail || !here ? pager.tail : here.closest('.mp-group'); const to = from && (v > 0 ? from.nextElementSibling : from.previousElementSibling); - if (to) pager.scroller.scrollTo({ top: to.offsetTop, behavior: 'smooth' }); + if (to) pagerGlide(pager.scroller, 'y', to.offsetTop); return; } if (hz) { e.preventDefault(); const to = !pager.onTail && here && (hz > 0 ? here.nextElementSibling : here.previousElementSibling); - if (to) here.parentElement.scrollTo({ left: to.offsetLeft, behavior: 'smooth' }); + if (to) pagerGlide(here.parentElement, 'x', to.offsetLeft); return; } if (e.key === ' ' && pager.cur) { -- cgit v1.2.3