From 8a5118839502adb52cd504867bb8ac602160a48c Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 14 Oct 2026 00:15:58 +0000 Subject: Feed clips: the player's full-screen button works again, and opens the pager controlslist=nofullscreen kept Chrome's full screen (no next clip) off the feed, but greyed the button out. The button is back; pressing it backs out of Chrome's full screen and opens the pager on that clip instead. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KPw21NEpWeTGu6swVASmBt --- src/features/messages.js | 18 +++++++++++++----- tools/fullscreen-notice-test.js | 12 +++++++++++- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/features/messages.js b/src/features/messages.js index 55a4376..16ae3a2 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -7662,7 +7662,17 @@ export function messagesFeature(ctx) { return true; } if (typeof document !== 'undefined') document.addEventListener('fullscreenchange', () => { - if (fsElement()) return; + const el = fsElement(); + // The player's own full-screen button on a feed clip: the pager, not + // Chrome's full screen (no next clip there). The button stays on the bar + // (left off, Chrome greys it out) and is taken over here. + if (el && !pager && el.matches && el.matches('.notes-feed video.note-video') && el.dataset.clip) { + const url = el.dataset.clip; + Promise.resolve((document.exitFullscreen || document.webkitExitFullscreen || (() => {})).call(document)).catch(() => {}) + .then(() => { if (!pager && document.contains(el)) openClip(el, url); }); + return; + } + if (el) return; // out of the pager's full screen some other way (Back on Android, Escape // on a computer): out of the pager too, in the one step if (pagerFs) { pagerFs = false; if (pager) dismissPager(); } @@ -7676,10 +7686,8 @@ export function messagesFeature(ctx) { // play button: no seek bar, no sound button. Without autoplay the // player's own controls are the way in, so they stay. const idle = autoplayOk() && !wokeClips.has(url); - // In the feed a tap on the clip opens the pager, so the player's own - // full-screen button (Chrome's full screen: no next clip, and its notice) - // is left off its bar there. - const v = h('video', { src: playSrc(url), class: 'note-video', controls: idle ? undefined : true, controlslist: ui.chatOpen && ui.msgView === 'feed' && !ui.noteThread && !ui.profilePk ? 'nofullscreen' : undefined, poster: videoThumbs.get(url) || (videoInfo.get(url) || {}).image || serverThumb(url), + // (data-clip: which clip it is, for the full-screen button's pager) + const v = h('video', { src: playSrc(url), class: 'note-video', controls: idle ? undefined : true, 'data-clip': url, poster: videoThumbs.get(url) || (videoInfo.get(url) || {}).image || serverThumb(url), preload: 'none', playsinline: true, muted: loud ? undefined : true, style: stable ? videoBoxStyle(feedMedia(url)) : undefined, // learned now, used by the next paint of any row with this clip diff --git a/tools/fullscreen-notice-test.js b/tools/fullscreen-notice-test.js index c5658ea..cf15f8c 100644 --- a/tools/fullscreen-notice-test.js +++ b/tools/fullscreen-notice-test.js @@ -93,7 +93,7 @@ async function run(lockOk) { await waitText('receive', 20000); await page.evaluate(() => document.querySelector('.app-nav-button[aria-label="Feed"]').click()); await page.waitForSelector('.notes-feed video.note-video', { timeout: 20000 }); - check('a feed clip\'s bar has no full-screen button', await waitFor(() => [...document.querySelectorAll('.notes-feed video.note-video')].every((v) => v.getAttribute('controlslist') === 'nofullscreen'), null, 5000)); + check('a feed clip\'s bar keeps its full-screen button (not greyed out)', await page.evaluate(() => [...document.querySelectorAll('.notes-feed video.note-video')].every((v) => !/nofullscreen/.test(v.getAttribute('controlslist') || '')))); await openClip('a wide one'); await sleep(1200); let s = await st(); @@ -103,6 +103,16 @@ async function run(lockOk) { await page.keyboard.press('ArrowDown'); await sleep(1200); await page.keyboard.press('Escape'); check('Escape closes it', await waitFor(() => !document.querySelector('.media-pager') && !document.fullscreenElement, null, 3000)); + // the player's own full-screen button: the pager, on that clip + await page.evaluate(() => { const r = [...document.querySelectorAll('.notes-feed [data-key]')].find((n) => n.textContent.includes('a tall one')); r.querySelector('video.note-video').scrollIntoView({ block: 'center' }); }); + await sleep(400); + await page.evaluate(() => [...document.querySelectorAll('.notes-feed [data-key]')].find((n) => n.textContent.includes('a tall one')).querySelector('video.note-video').requestFullscreen()); + check('its full-screen button opens the pager', await waitFor(() => !!document.querySelector('.media-pager.videos'), null, 4000), JSON.stringify(await st())); + check('…on that clip', await waitFor(() => { const s = [...document.querySelectorAll('.mp-slide')].find((x) => { const r = x.getBoundingClientRect(); return Math.abs(r.top) < 4 && Math.abs(r.left) < 4; }); return !!s && /tall/.test(s.getAttribute('data-k') || ''); }, null, 3000)); + check('…and the clip itself isn\'t left full screen', await waitFor(() => !(document.fullscreenElement && document.fullscreenElement.matches('video.note-video')), null, 3000)); + await page.keyboard.press('Escape'); + check('Escape closes that too', await waitFor(() => !document.querySelector('.media-pager'), null, 3000)); + await sleep(600); await openClip('a tall one'); await sleep(1200); s = await st(); check('a tall clip: the pager, no full screen, the screen left alone', s.open && !s.fs && !s.orient.some((x) => x.startsWith('lock')), JSON.stringify(s)); -- cgit v1.2.3