aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-17 05:19:51 -0700
committerAdam Soltys <[email protected]>2026-10-17 05:19:51 -0700
commitd44a61a2b4e6b59ed6bebfe64d6b2f3a1dbb303d (patch)
tree82ae5486fc4e4c8e7d6c0df448317b11a4c28ca0
parentebbe49017af189a42cd1f30c808fa64b936ff4e8 (diff)
downloadcoinosv3-d44a61a2b4e6b59ed6bebfe64d6b2f3a1dbb303d.tar.gz
coinosv3-d44a61a2b4e6b59ed6bebfe64d6b2f3a1dbb303d.zip
Pager flicks are judged by when the finger moved, not when the move was handled; the counter counts the reachable posts again.
Right after a refresh, with clips warming, touch moves queue behind other work and arrive in bursts. The flick's speed was timed on handling, so a burst after a stall read as a fling (a short flick flew) and a stall before the last move as a crawl (a long flick took one step) (Adam, 2026-10-17: "sometimes a quick long flick only proceeds to the next video and doesn't coast as i would expect it to and sometimes the opposite happens -- a short flick does fly"). Each move is now timed by its event's timeStamp, when the finger made it; moves that share one (synthetic ones can) fall back to the handling time. The counter goes back to counting the posts in the carousel (e529ab1 undone): counting the ones still warming confused, since they can't be scrolled to (Adam: "maybe we should go back to showing the total loaded"). tools/pager-swipe-fly-test.js: a gentle swipe whose moves are handled in one burst goes one post; a fast flick whose last move is handled 300 ms late still flies (it took one step with the old timing). pager-ready-gate is back to "the counter grew". pager-ready-gate, pager-tap-slop, pager-seek-hide, pager-first-frame, thread-pager and profile-pager pass. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
-rw-r--r--src/features/messages.js23
-rw-r--r--tools/pager-ready-gate-test.js2
-rw-r--r--tools/pager-swipe-fly-test.js26
3 files changed, 42 insertions, 9 deletions
diff --git a/src/features/messages.js b/src/features/messages.js
index 0ea7520..76330a4 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -13194,14 +13194,12 @@ export function messagesFeature(ctx) {
const slide = rowSlide(group);
if (slide) { pagerShow(slide); pagerArrive(slide); }
}
- // Counted: every post the pager has found, the ones still waiting for
- // their clip to be ready included — only those kept out for their shape
- // aren't. Counting the carousel alone, a refresh showed "1 / 2" over a
- // grid of a dozen clips, the total creeping up as clips warmed (Adam,
- // 2026-10-17: "the total count persists").
+ // Counted: the posts in the carousel — the ones that can be reached. (All
+ // the posts found, the waiting ones too, was tried and undone: a count
+ // that can't be scrolled to confused — Adam, 2026-10-17.)
function pagerPosition() {
if (!pager) return;
- const groups = pagerGroups(true).filter((g) => !g.classList.contains('mp-off'));
+ const groups = pagerGroups();
const slide = pager.cur || pager.anchor;
const index = groups.indexOf(slide && slide.closest('.mp-group'));
pager.el.querySelector('.mp-position').textContent = (index < 0 ? 0 : index + 1) + ' / ' + groups.length;
@@ -13312,7 +13310,15 @@ export function messagesFeature(ctx) {
const delta = d.axis === 'y' ? dy : dx;
d.far = Math.max(d.far || 0, Math.hypot(dx, dy));
if (d.axis === 'y') d.box.scrollTop = d.start - delta; else d.box.scrollLeft = d.start - delta;
- d.track.push({ t: performance.now(), v: delta }); // (when handled: synthetic moves can share a timeStamp)
+ // Timed when the finger moved (the event's timeStamp), not when the
+ // move was handled: right after a refresh, with clips warming, moves
+ // queue behind other work and arrive in bursts — timed on handling, a
+ // burst after a stall read as a fling (a short flick flew) and a
+ // stall at the end as a crawl (a long flick took one step) (Adam,
+ // 2026-10-17: "sometimes a quick long flick only proceeds to the next
+ // video … sometimes … a short flick does fly"). The handling time is
+ // kept for moves that share a timeStamp (synthetic ones can).
+ d.track.push({ t: e.timeStamp, h: performance.now(), v: delta });
while (d.track.length > 2 && d.track[d.track.length - 1].t - d.track[1].t >= TRACK_MS) d.track.shift();
}, { passive: false });
// let go of a drag: on to the next (or the one before), or back
@@ -13349,7 +13355,8 @@ export function messagesFeature(ctx) {
const delta = d.axis === 'y' ? t.clientY - d.y : t.clientX - d.x;
const size = d.axis === 'y' ? d.box.clientHeight : d.box.clientWidth;
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 dt = a && b ? (b.t - a.t >= 8 ? b.t - a.t : b.h - a.h) : 0;
+ const vel = dt > 0 ? (b.v - a.v) / dt : 0;
const tap = d.far < TAP_SLOP && Date.now() - d.at < TAP_MS;
const go = !tap && (Math.abs(delta) > size * (d.axis === 'y' ? SWIPE_Y : SWIPE_X) || (Math.abs(delta) >= FLICK_MIN && Math.abs(vel) > FLICK && Math.sign(vel) === Math.sign(delta)));
settle(d, go ? (delta < 0 ? 1 : -1) : 0, go ? vel : 0);
diff --git a/tools/pager-ready-gate-test.js b/tools/pager-ready-gate-test.js
index 4138e42..d6df826 100644
--- a/tools/pager-ready-gate-test.js
+++ b/tools/pager-ready-gate-test.js
@@ -118,7 +118,7 @@ try {
await sleep(300);
const g = await groups();
check('…joining above the clip on screen without moving it', g.find((x) => x.cur)?.clip === 'tall3' && Math.abs(g.find((x) => x.cur).top) <= 1, JSON.stringify(g.map((x) => x.clip + ':' + x.top + (x.cur ? '*' : ''))));
- check('…and the counter counted it all along (3 / 4 before and after)', n0 === '3 / 4' && (await counter()) === '3 / 4', n0 + ' → ' + (await counter()));
+ check('…and the counter grew', (await counter()) !== n0, n0 + ' → ' + (await counter()));
await page.keyboard.press('ArrowUp');
check('a step back up lands on the one that joined', await waitFor(() => { const s = document.querySelector('.mp-slide.mp-cur'); return !!s && /tall2/.test(s.getAttribute('data-k')) && !s.querySelector('video').paused; }, null, 6000), String(await curClip()));
diff --git a/tools/pager-swipe-fly-test.js b/tools/pager-swipe-fly-test.js
index aec8fdd..cae3536 100644
--- a/tools/pager-swipe-fly-test.js
+++ b/tools/pager-swipe-fly-test.js
@@ -58,6 +58,23 @@ const swipe = async (px, ms, steps = 6) => {
fire('touchend', 700 - px, true);
}, [px, ms, steps]);
};
+// A swipe whose moves are made on schedule (each event stamped as the finger
+// moved) but handled late, the main thread busy: held back and delivered in
+// a burst at the end (burst), or the last move and the lift after a stall.
+const queued = (px, ms, steps, mode) => page.evaluate(([px, ms, steps, mode]) => {
+ const el = document.querySelector('.mp-scroll'), target = document.elementFromPoint(195, 700) || el;
+ const touch = (y) => new Touch({ identifier: 1, target, clientX: 195, clientY: y, pageX: 195, pageY: y });
+ const make = (type, y, up) => new TouchEvent(type, { bubbles: true, cancelable: true, touches: up ? [] : [touch(y)], changedTouches: [touch(y)], targetTouches: up ? [] : [touch(y)] });
+ const t0 = performance.now(), evs = [make('touchstart', 700)];
+ for (let i = 1; i <= steps; i++) { while (performance.now() < t0 + ms * i / steps); evs.push(make('touchmove', 700 - px * i / steps)); }
+ evs.push(make('touchend', 700 - px, true));
+ if (mode === 'burst') { for (const e of evs) target.dispatchEvent(e); return; }
+ // stall: all but the last move handled as they come, then 300 ms busy
+ const tail = evs.splice(-2);
+ for (const e of evs) target.dispatchEvent(e);
+ const t1 = performance.now(); while (performance.now() < t1 + 300);
+ for (const e of tail) target.dispatchEvent(e);
+}, [px, ms, steps, mode]);
try {
await page.setViewport({ width: 390, height: 844, hasTouch: true, isMobile: true });
await page.goto(server.url.href, { waitUntil: 'domcontentloaded' });
@@ -111,6 +128,15 @@ try {
await swipe(320, 130); await settled();
check('a gentle swipe from the first: the second, as ever', (await at()) === 2, String(await at()));
+ console.log('\n[a busy main thread: the speed is the finger\'s, not the handling\'s]');
+ // (the dispatch loop runs all the events in one go: built on schedule, each stamped as it was made)
+ while ((await at()) !== 1) { await page.keyboard.press('ArrowUp'); await settled(); }
+ await queued(200, 200, 8, 'burst'); await settled();
+ check('from post 1, a gentle 200px swipe over 200ms, its moves handled in one burst: one post on', (await at()) === 2, String(await at()));
+ await queued(600, 80, 4, 'stall'); await settled();
+ check('a fast flick whose last move was handled 300ms late still flies', typeof (await at()) === 'number' && (await at()) >= 5, String(await at()));
+ while ((await at()) !== 1) { await page.keyboard.press('ArrowUp'); await settled(); }
+
console.log('\n[a finger down mid-flight catches it]');
await swipe(600, 80, 3);
await sleep(120);