From e6a65e9921bc0e61ea17459e4bd12f64baf0c3b8 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sat, 10 Oct 2026 07:18:22 -0700 Subject: Videos: a tap on the picture goes full screen. The corner full-screen button is a reach on a phone, so a tap anywhere on a feed clip — outside the player's own big play button and control bar, judged from the tap's position since those live inside the video element — now goes full screen (a wide clip asks for landscape). Full screen, a tap pauses a playing clip, and a tap on a paused one (again outside its play button and controls) comes back out. A video's taps no longer reach the post either: a tap on the player's controls used to open the thread, which in full screen took the player out from under the reader. tools/video-tap-test.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3 --- src/features/messages.js | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) (limited to 'src/features') diff --git a/src/features/messages.js b/src/features/messages.js index 358bd2e..a3452ee 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -7285,6 +7285,46 @@ export function messagesFeature(ctx) { } }); } + // A tap on the picture itself (not the player's own play button or its + // control bar): inline, it goes full screen — the corner button is a + // reach on a phone. Full screen, it pauses a playing clip, and a tap on a + // paused one comes back out. The player's controls are inside the video + // element, so where they sit is judged from the tap's position. + const fsElement = () => (typeof document !== 'undefined' && (document.fullscreenElement || document.webkitFullscreenElement)) || null; + function videoTap(e, url) { + const v = e.currentTarget; + if (!v || v.tagName !== 'VIDEO') return; + const r = v.getBoundingClientRect(); + const x = e.clientX - r.left, y = e.clientY - r.top; + // a video's taps are its own: never the post's (which opened the thread + // and, full screen, took the player out from under the reader) + e.stopPropagation(); + const fs = fsElement() === v; + // full screen, the video's box is the screen; its controls sit along the bottom + const H = fs ? window.innerHeight : r.height, W = fs ? window.innerWidth : r.width; + if (v.controls && y > H - 64) return; // the control bar + if (v.controls && v.paused && Math.hypot(x - W / 2, y - H / 2) < 48) return; // the big play button + e.preventDefault(); + if (fs) { + if (!v.paused) v.pause(); + else (document.exitFullscreen || document.webkitExitFullscreen || (() => {})).call(document)?.catch?.(() => {}); + return; + } + // a clip still idle (autoplaying, no controls) becomes the reader's + wokeClips.add(url); + const box = v.closest('.note-video-box'); + if (box) { box.classList.remove('idle'); box._skipMorph = true; } + v.controls = true; + const go = v.requestFullscreen || v.webkitRequestFullscreen || v.webkitEnterFullscreen; + if (!go) return; + Promise.resolve(go.call(v)).then(() => { + // a wide clip fills a phone on its side + if (v.videoWidth > v.videoHeight) screen.orientation?.lock?.('landscape').catch(() => {}); + }).catch(() => {}); + } + if (typeof document !== 'undefined') document.addEventListener('fullscreenchange', () => { + if (!fsElement()) { try { screen.orientation?.unlock?.(); } catch {} } + }); function videoNode(url, { stable = false } = {}) { const loud = unmutedClips.has(url); // Nothing is fetched until the clip is about to play (preload none): a @@ -7299,6 +7339,7 @@ export function messagesFeature(ctx) { style: stable ? videoBoxStyle(feedMedia(url)) : undefined, // learned now, used by the next paint of any row with this clip onLoadedmetadata: (e) => rememberVideo(url, e.target.videoWidth, e.target.videoHeight), + onClick: (e) => videoTap(e, url), onError: (e) => { if (!stable && !(e.target.getAttribute('src') || '').startsWith(VMEDIA)) { const b = e.target.parentElement; if (b) b.style.display = 'none'; } } }); guardPicture(v, url); v.muted = !loud; // the property, not just the attribute: a script-made element autoplays only muted -- cgit v1.2.3