From b054dfd07e9313eca3df5d052b4f32cc4460ac48 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Mon, 19 Oct 2026 22:52:56 -0700 Subject: Videos: the seek bar is out of sight (and out of reach) while a clip plays. A tap near the foot of the screen brings it up for a few seconds without pausing (longer while it's dragged), and a paused clip shows it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg --- src/features/messages.js | 18 +++++++++++++++--- src/style.css | 5 ++++- tools/feed-video-pager-test.js | 3 +++ tools/pager-seek-hide-test.js | 26 +++++++++++++++++++++----- 4 files changed, 43 insertions(+), 9 deletions(-) diff --git a/src/features/messages.js b/src/features/messages.js index 030d397..02e418d 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -14487,6 +14487,12 @@ export function messagesFeature(ctx) { // (the clip holds still meanwhile, and the time shows); the drag is the // bar's alone, never the pager's swipe or its tap. const clock = (t) => { t = Math.max(0, Math.floor(t || 0)); const m = Math.floor(t / 60), x = t % 60; return m + ':' + String(x).padStart(2, '0'); }; + // up for a few seconds, and on while a finger is on it + function revealSeek(bar) { + bar.classList.add('reveal'); + clearTimeout(bar._hide); + bar._hide = setTimeout(() => { if (bar._drag) revealSeek(bar); else bar.classList.remove('reveal'); }, 3500); + } function seekBar(v) { const fill = h('div', { class: 'mp-seek-fill' }); const time = h('span', { class: 'mp-seek-time' }); @@ -14517,6 +14523,7 @@ export function messagesFeature(ctx) { if (!d || d.id !== e.pointerId) return; bar._drag = null; bar.classList.remove('drag'); + if (bar.classList.contains('reveal')) revealSeek(bar); // a few seconds more after the drag if (e.type === 'pointerup') to(frac(e)); if (d.playing) v.play().catch(() => {}); }; @@ -15245,7 +15252,7 @@ export function messagesFeature(ctx) { pendingTap = setTimeout(() => { pendingTap = 0; const btn = d.target.closest && d.target.closest('button, a'); - if (btn) btn.click(); else pagerTap(d.target); // (the click here is before tapDoneAt: not swallowed) + if (btn) btn.click(); else pagerTap(d.target, t.clientY); // (the click here is before tapDoneAt: not swallowed) tapDoneAt = Date.now(); }, 120); }, { passive: true }); @@ -15260,11 +15267,16 @@ export function messagesFeature(ctx) { if (pendingTap) { clearTimeout(pendingTap); pendingTap = 0; } else if (Date.now() - tapDoneAt < 700) { e.stopPropagation(); e.preventDefault(); } }, true); - el.addEventListener('click', (e) => pagerTap(e.target)); + el.addEventListener('click', (e) => pagerTap(e.target, e.clientY)); // a tap shows (or hides) who posted it and what they said - function pagerTap(target) { + function pagerTap(target, y) { if (target.closest('button, a, .mp-caption, .mp-seek')) return; if (Date.now() - videoTapAt < 800) return; // the click of the touch that opened it + // The seek bar is out of sight while a clip plays (Adam, 2026-10-19: + // only when someone goes for it, or pauses): a tap near the foot of + // the screen, where it sits, brings it up without pausing. + const bar = pager && video && pager.cur && pager.cur.querySelector('.mp-seek.ready'); + if (bar && y != null && !el.classList.contains('show') && y >= bar.getBoundingClientRect().top - 72) { revealSeek(bar); return; } if (pager && video) { // A clip (from the feed or the media grid): a tap anywhere but the // seek bar and the buttons pauses it and brings up who posted it, diff --git a/src/style.css b/src/style.css index 4a5661e..6e1097a 100644 --- a/src/style.css +++ b/src/style.css @@ -1729,7 +1729,10 @@ body::after { position: absolute; left: 16px; right: 16px; bottom: var(--mp-seek-lift); z-index: 3; height: 28px; display: flex; align-items: flex-end; touch-action: none; cursor: pointer; visibility: hidden; opacity: 0; } -.mp-seek.ready { visibility: visible; opacity: 1; transition: opacity .18s; } +/* out of sight (and out of reach: a tap there must not jump the clip) while + it plays; up when paused (.show), after a tap near the foot (.reveal), or held */ +.mp-seek.ready { visibility: visible; opacity: 0; pointer-events: none; transition: opacity .18s; } +.media-pager.show .mp-seek.ready, .mp-seek.ready.reveal, .mp-seek.ready.drag { opacity: 1; pointer-events: auto; } /* only the clip on screen's bar, and not while the pager is in motion (a finger, a glide, a flight): it goes at once and fades back in on landing */ .mp-slide:not(.mp-cur) .mp-seek, .media-pager.moving .mp-seek { opacity: 0; transition: none; pointer-events: none; } diff --git a/tools/feed-video-pager-test.js b/tools/feed-video-pager-test.js index add9208..cccde41 100644 --- a/tools/feed-video-pager-test.js +++ b/tools/feed-video-pager-test.js @@ -120,6 +120,9 @@ try { check('the seek bar sits clear of the bottom edge', geo.bottom >= 32, JSON.stringify(geo)); check('…and in from the sides', geo.left >= 16 && geo.right >= 16, JSON.stringify(geo)); if (process.env.SHOT) { await page.evaluate(() => document.querySelector('.media-pager').classList.add('show')); await sleep(300); await page.screenshot({ path: process.env.SHOT }); await page.evaluate(() => document.querySelector('.media-pager').classList.remove('show')); } + // (out of sight while it plays: a tap near the foot brings it up) + { const r = await page.evaluate((cur) => { const b = eval(cur)().querySelector('.mp-seek').getBoundingClientRect(); return { x: b.x + b.width / 2, y: b.y - 30 }; }, '(' + CUR + ')'); await page.mouse.click(r.x, r.y); await sleep(300); } + check('a tap near the foot brings the seek bar up', await page.evaluate((cur) => getComputedStyle(eval(cur)().querySelector('.mp-seek')).opacity === '1', '(' + CUR + ')')); const bar = await page.evaluate((cur) => { const r = eval(cur)().querySelector('.mp-seek').getBoundingClientRect(); return { x: r.x, y: r.y + r.height - 4, w: r.width }; }, '(' + CUR + ')'); await page.mouse.move(bar.x + bar.w * 0.1, bar.y); await page.mouse.down(); diff --git a/tools/pager-seek-hide-test.js b/tools/pager-seek-hide-test.js index 64ff7b5..d5d527b 100644 --- a/tools/pager-seek-hide-test.js +++ b/tools/pager-seek-hide-test.js @@ -2,7 +2,10 @@ // while the pager is in motion — a glide to the next clip, a finger // dragging the slides, the snap back — no seek bar shows at all (it rode // along with the slide, a white line sliding up the screen), and it fades -// back in once the clip has landed. The position counter is the other way +// back in once the clip has landed. And at rest it's out of sight too while +// the clip plays (Adam, 2026-10-19): a tap near the foot of the screen brings +// it up for a few seconds without pausing, and a paused clip shows it. The +// position counter is the other way // round: it shows while the pager moves and fades out once it has settled. // Run: bun tools/pager-seek-hide-test.js import puppeteer from './puppeteer.js'; @@ -88,10 +91,21 @@ try { await page.mouse.click(at.x, at.y); check('the pager opens', await waitFor(() => !!document.querySelector('.media-pager.videos .mp-slide'), null, 4000)); - console.log('\n[at rest: the clip on screen has its seek bar]'); + console.log('\n[at rest: the seek bar comes when asked for]'); check('the clip on screen plays', await waitFor(() => [...document.querySelectorAll('.mp-slide.mp-cur video.mp-media')].some((v) => !v.paused), null, 8000)); - check('…and its seek bar shows', await waitFor(() => { const b = document.querySelector('.mp-slide.mp-cur .mp-seek'); return !!b && b.classList.contains('ready') && getComputedStyle(b).opacity === '1'; }, null, 6000), JSON.stringify(await bars())); + await waitFor(() => !!document.querySelector('.mp-slide.mp-cur .mp-seek.ready'), null, 6000); + await sleep(400); + const curBar = () => page.evaluate(() => { const b = document.querySelector('.mp-slide.mp-cur .mp-seek'); const v = document.querySelector('.mp-slide.mp-cur video.mp-media'); return { opacity: getComputedStyle(b).opacity, pe: getComputedStyle(b).pointerEvents, playing: !v.paused }; }); + check('…its seek bar out of sight (and out of reach) while it plays', (await curBar()).opacity === '0' && (await curBar()).pe === 'none', JSON.stringify(await curBar())); + await page.mouse.click(195, 844 - 70); // near the foot, where the bar sits + check('a tap near the foot brings it up, the clip playing on', await waitFor(() => { const b = document.querySelector('.mp-slide.mp-cur .mp-seek'); return getComputedStyle(b).opacity === '1'; }, null, 2000) && (await curBar()).playing, JSON.stringify(await curBar())); check('…no other', (await seenBars()).length === 1, (await seenBars()).join(' ')); + check('…and it goes again a few seconds later', await waitFor(() => getComputedStyle(document.querySelector('.mp-slide.mp-cur .mp-seek')).opacity === '0', null, 6000), JSON.stringify(await curBar())); + await page.mouse.click(195, 380); + check('a tap mid-screen pauses the clip, and the bar is up', await waitFor(() => { const b = document.querySelector('.mp-slide.mp-cur .mp-seek'); return getComputedStyle(b).opacity === '1'; }, null, 2000) && !(await curBar()).playing, JSON.stringify(await curBar())); + await page.mouse.click(195, 380); + check('…another plays it on, the bar gone', await waitFor(() => getComputedStyle(document.querySelector('.mp-slide.mp-cur .mp-seek')).opacity === '0', null, 2000) && (await curBar()).playing, JSON.stringify(await curBar())); + await page.mouse.click(195, 844 - 70); // up again, for the glide check('the pager is not marked moving', await page.evaluate(() => !document.querySelector('.media-pager').classList.contains('moving'))); check('the position counter has faded out', await waitFor(() => +getComputedStyle(document.querySelector('.mp-position')).opacity < 0.01, null, 4000), String(await counterOpacity())); @@ -106,11 +120,13 @@ try { check('…and not one seek bar to be seen', between.every((f) => f.seen === 0), between.map((f) => f.seen).join('')); const landedOn = await waitFor(() => { const s = document.querySelector('.mp-slide.mp-cur'); return !!s && s.getAttribute('data-k').endsWith('/tall.webm'); }, null, 4000); check('it lands on the next clip', landedOn); - check('…whose seek bar fades back in', await waitFor(() => { const b = document.querySelector('.mp-slide.mp-cur .mp-seek'); return !!b && getComputedStyle(b).opacity === '1'; }, null, 6000), JSON.stringify(await bars())); + await sleep(600); + check('…whose seek bar stays out of sight until asked for', await page.evaluate(() => { const b = document.querySelector('.mp-slide.mp-cur .mp-seek'); return !!b && getComputedStyle(b).opacity === '0'; }), JSON.stringify(await bars())); check('…the one before, now off screen, shows none', (await bars()).filter((x) => x.clip === 'wide.webm').every((x) => !x.seen), JSON.stringify(await bars())); check('the pager is not marked moving once landed', await page.evaluate(() => !document.querySelector('.media-pager').classList.contains('moving'))); console.log('\n[a finger holding the slides off their mark: none either]'); + await page.mouse.click(195, 844 - 70); await sleep(300); // the bar up first await page.touchscreen.touchStart(195, 600); await page.touchscreen.touchMove(195, 560); await sleep(30); await page.touchscreen.touchMove(195, 500); await sleep(250); @@ -119,7 +135,7 @@ try { check('…and the position counter is back', (await counterOpacity()) === 1, String(await counterOpacity())); await page.touchscreen.touchMove(195, 590); await sleep(30); await page.touchscreen.touchEnd(); - check('let go, it snaps back and the bar returns', await waitFor(() => { const b = document.querySelector('.mp-slide.mp-cur .mp-seek'); const el = document.querySelector('.media-pager'); return !!b && getComputedStyle(b).opacity === '1' && !el.classList.contains('moving') && document.querySelector('.mp-slide.mp-cur').getAttribute('data-k').endsWith('/tall.webm'); }, null, 4000), JSON.stringify(await bars())); + check('let go, it snaps back', await waitFor(() => { const b = document.querySelector('.mp-slide.mp-cur .mp-seek'); const el = document.querySelector('.media-pager'); return !!b && !el.classList.contains('moving') && document.querySelector('.mp-slide.mp-cur').getAttribute('data-k').endsWith('/tall.webm'); }, null, 4000), JSON.stringify(await bars())); await sleep(500); check('a moment after, the counter still shows', (await counterOpacity()) > 0.99, String(await counterOpacity())); check('…then fades out', await waitFor(() => +getComputedStyle(document.querySelector('.mp-position')).opacity < 0.01, null, 4000), String(await counterOpacity())); -- cgit v1.2.3