diff options
| author | Adam Soltys <[email protected]> | 2026-10-17 08:36:39 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-17 08:36:39 -0700 |
| commit | eb5e2e65af5e9fccc48fecbe839f7b1e35e1b059 (patch) | |
| tree | 5d1d4523302e54ecc7fa1d07eb2d390f407067ed | |
| parent | bfcdc0beb9b0016aa0c58043c993eaad73825fe9 (diff) | |
| download | coinosv3-eb5e2e65af5e9fccc48fecbe839f7b1e35e1b059.tar.gz coinosv3-eb5e2e65af5e9fccc48fecbe839f7b1e35e1b059.zip | |
A touch stops a pager flight on the post most on screen: a tap only stops it, a swipe back from there is the post before, and one the way it was flying takes it on.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
| -rw-r--r-- | src/features/messages.js | 21 | ||||
| -rw-r--r-- | tools/pager-swipe-fly-test.js | 47 |
2 files changed, 54 insertions, 14 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index 82810c3..254ec66 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -13184,6 +13184,7 @@ export function messagesFeature(ctx) { const dest = pagerAt(pager.scroller, to, true); const screens = Math.abs(dest - pager.scroller.scrollTop) / Math.max(1, pager.scroller.clientHeight); pager.flying = true; + pager.flyDir = Math.sign(dest - pager.scroller.scrollTop); const land = pager.kind === 'videos' && !frugalData() && rowSlide(to); // the landing's first frame, fetched on the way if (land) slideFrame(land, -1); // A flight eases out on a square, not a cube: on a cube a long one @@ -13199,6 +13200,14 @@ export function messagesFeature(ctx) { }); } const pagerArrive = (slide) => { if (pager && pager.arrive) pager.arrive(slide); }; + // A finger down on a flight stops it there and then, settling on the post + // most on screen (Adam, 2026-10-17: "Tapping … should instantly stop the + // flying … and snap to the video that's most on screen"). + function pagerCatch() { + pagerHold(pager.scroller); // the flight's onDone: no longer flying, arrived at the post most on screen + const here = pagerHere(), g = here && here.closest('.mp-group'); + if (g) pagerGlide(pager.scroller, 'y', pagerAt(pager.scroller, g, true)); + } // the sideways arrows show only where the post has a picture that way function pagerArrows(slide) { if (!pager || !slide) return; @@ -13325,10 +13334,16 @@ export function messagesFeature(ctx) { let touch = null, pendingTap = 0, tapDoneAt = 0; el.addEventListener('touchstart', (e) => { const t = e.touches[0]; + // caught in flight: stopped where it is (a two-finger touch, a touch + // on the seek bar too); a swipe back from there is the post before it + // however hard (Adam, 2026-10-17: "reverse swiping should go to the + // previous video"), one on the way it was flying takes it on + const caught = !!pager && pager.flying && !!pager.scroller._glide ? pager.flyDir || 0 : 0; + if (caught) pagerCatch(); const img = e.target.closest && e.target.closest('img.mp-media'); // not a touch on the seek bar, in an opened caption, or on a picture zoomed in (each its own) touch = e.touches.length === 1 && pager && !e.target.closest('.mp-seek, .mp-caption.open') && !(img && img.style.touchAction === 'none') - ? { x: t.clientX, y: t.clientY, at: Date.now(), slide: pager.cur, target: e.target } : null; + ? { x: t.clientX, y: t.clientY, at: Date.now(), slide: pager.cur, target: e.target, caught } : null; }, { passive: true }); el.addEventListener('touchmove', (e) => { const d = touch; @@ -13366,7 +13381,7 @@ export function messagesFeature(ctx) { if (!stops.length) return; // the stop it set off from, then one along (or back) — or, flung, several const from = stops.reduce((bi, p, i) => (Math.abs(p - d.start) < Math.abs(stops[bi] - d.start) ? i : bi), 0); - const posts = y && step && Math.abs(vel) >= FLY_V && pagerCanFly() ? Math.min(FLY_MAX, Math.max(1, Math.round(Math.abs(vel) * FLY_TAU / Math.max(1, box.clientHeight)))) : 1; + const posts = y && step && Math.abs(vel) >= FLY_V && pagerCanFly() && !(d.caught && step !== d.caught) ? Math.min(FLY_MAX, Math.max(1, Math.round(Math.abs(vel) * FLY_TAU / Math.max(1, box.clientHeight)))) : 1; const next = from + step * posts; // Past either end, round to the other: the first post (going up, the // last) — a step, or a fling whose coast is mostly beyond the end. A @@ -13400,6 +13415,8 @@ export function messagesFeature(ctx) { settle(d, go ? (delta < 0 ? 1 : -1) : 0, go ? vel : 0); if (!tap) return; } + // the tap that caught a flight only stopped it (the buttons still work) + if (d.caught && !(d.target.closest && d.target.closest('button, a'))) { tapDoneAt = Date.now(); return; } if (Date.now() - d.at > 500) return; // a press, not a tap clearTimeout(pendingTap); pendingTap = setTimeout(() => { diff --git a/tools/pager-swipe-fly-test.js b/tools/pager-swipe-fly-test.js index 6f62007..707aad6 100644 --- a/tools/pager-swipe-fly-test.js +++ b/tools/pager-swipe-fly-test.js @@ -1,8 +1,9 @@ // On a phone, a fast flick up or down in the media pager flies past several // posts and lands snapped on one, the way a spin of the wheel does; a // gentler swipe is one post, as before; a fling at the end lands on the -// last post rather than wrapping past it; a finger put down mid-flight -// catches it. Run: bun tools/pager-swipe-fly-test.js +// last post rather than wrapping past it; a tap mid-flight stops it on the +// post most on screen, and a swipe back from there goes to the one before. +// Run: bun tools/pager-swipe-fly-test.js import puppeteer from './puppeteer.js'; import { cacheKeyFor } from '../src/wallet.js'; import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure'; @@ -141,17 +142,39 @@ try { 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]'); + console.log('\n[a tap mid-flight stops it on the post most on screen]'); + // the post most on screen as the finger comes down (its number), then the touch, in one go + const down = () => page.evaluate(() => { + const sc = document.querySelector('.mp-scroll'), y = sc.scrollTop; + const g = [...sc.querySelectorAll(':scope > .mp-group:not(.mp-wait):not(.mp-off)')].reduce((b, x) => (Math.abs(x.offsetTop - y) < Math.abs(b.offsetTop - y) ? x : b)); + const target = window.__tt = document.elementFromPoint(195, 500), t = new Touch({ identifier: 2, target, clientX: 195, clientY: 500, pageX: 195, pageY: 500 }); + target.dispatchEvent(new TouchEvent('touchstart', { bubbles: true, cancelable: true, touches: [t], changedTouches: [t], targetTouches: [t] })); + return Number(/p(\d+)\.png/.exec(g.querySelector('.mp-slide').getAttribute('data-k'))[1]); + }); + // the same finger moving (to y), or lifted there + const finger = (y, up) => page.evaluate(([y, up]) => { + const target = window.__tt, t = new Touch({ identifier: 2, target, clientX: 195, clientY: y, pageX: 195, pageY: y }); + target.dispatchEvent(new TouchEvent(up ? 'touchend' : 'touchmove', { bubbles: true, cancelable: true, touches: up ? [] : [t], changedTouches: [t], targetTouches: up ? [] : [t] })); + }, [y, up]); await swipe(600, 80, 3); - await sleep(120); - const mid = await page.evaluate(() => document.querySelector('.mp-scroll').scrollTop); - await page.touchscreen.touchStart(195, 500); - await page.touchscreen.touchMove(195, 480); await sleep(30); - await page.touchscreen.touchMove(195, 470); await sleep(200); - const held = await page.evaluate(() => document.querySelector('.mp-scroll').scrollTop); - await page.touchscreen.touchEnd(); await settled(); - check('the flight stops under the finger', mid > 0 && Math.abs(held - mid) < 844, mid + ' → ' + held); - check('…and lands square on a post when it lifts', (await at()) !== 'between', String(await at())); + await sleep(150); + const flying = await page.evaluate(() => document.querySelector('.mp-scroll').scrollTop); + const most = await down(); + await sleep(60); await finger(500, true); + await sleep(250); + check('a tap mid-flight: on the post most on screen in a moment', flying > 0 && (await at()) === most, most + ' wanted, ' + (await at())); + await settled(); + check('…and stays there', (await at()) === most, String(await at())); + check('…the tap only stopped it (no details brought up)', !(await page.evaluate(() => document.querySelector('.media-pager').classList.contains('show')))); + + console.log('\n[a swipe back mid-flight: the post before]'); + while ((await at()) !== 1) { await page.keyboard.press('ArrowUp'); await settled(); } + await swipe(600, 80, 3); + await sleep(150); + const most2 = await down(); + for (const y of [520, 560, 640, 760]) { await finger(y); await sleep(20); } + await finger(760, true); await settled(); + check('caught on post ' + most2 + ' and swiped back: post ' + (most2 - 1), most2 > 1 && (await at()) === most2 - 1, String(await at())); check('no page errors', errs.length === 0, errs.slice(0, 2).join(' | ')); } catch (e) { console.error(e); ok = false; } finally { await browser.close(); server.stop(true); } console.log(ok ? '\n✅ a fast flick flies through the pager' : '\n❌ failed'); |
