diff --git a/frontend/js/feed.js b/frontend/js/feed.js index 9b99ed7..a3bd5c6 100644 --- a/frontend/js/feed.js +++ b/frontend/js/feed.js @@ -32,15 +32,14 @@ App.feed = App.feed || {}; let scrollBound = false; let scrollRaf = null; - // The slide height under which the current scroll position / top-spacer were - // last laid out. On an orientation change the viewport (and therefore the - // slide height) changes, which makes the browser re-snap the scroll-snap - // container and fire scroll events with positions that no longer map to the - // active slide. We compare against this so onScroll can ignore those - // resize-induced scroll events and let onResize realign to the active video - // instead -- the active slide must never change just because the device was - // rotated. It's updated only when we (re)align the scroll position ourselves. - let lastSlideHeight = 0; + // While true, scroll events are ignored. A viewport change (e.g. an + // orientation switch) makes the scroll-snap container re-snap and fire + // scroll events with positions that no longer map to the active slide; + // onResize sets this for the brief realign window so those events don't + // flip the active video -- rotating the device must never change which + // slide is playing. Normal swipes (no resize in flight) are unaffected. + let suppressScroll = false; + let resizeSettleRaf = null; // HUD auto-hide: the reels HUD fades out after this much inactivity and // reappears on any pointer movement / tap / scroll. Buttons keep their @@ -433,14 +432,10 @@ App.feed = App.feed || {}; scrollRaf = null; const scroller = getScroller(); if (!scroller) return; - const h = slideHeight(); - // A scroll that fires while the slide height differs from what the - // current layout was aligned to is a side effect of a viewport - // change (e.g. an orientation switch re-snapping the container), not - // a real user swipe. Ignore it and let onResize re-anchor to the - // active video, so rotating the device never jumps to another slide. - if (h !== lastSlideHeight) return; - const index = clampIndex(Math.round(scroller.scrollTop / h)); + // Ignore scroll events fired by a resize/orientation re-snap; the + // active video is realigned by onResize instead (see suppressScroll). + if (suppressScroll) return; + const index = clampIndex(Math.round(scroller.scrollTop / slideHeight())); if (index < 0) return; if (index !== state.feedActiveIndex) { setActive(index); @@ -470,18 +465,27 @@ App.feed = App.feed || {}; if (spacer) spacer.style.height = `${start * h}px`; const scroller = getScroller(); if (scroller) scroller.scrollTop = index * h; - lastSlideHeight = h; const activeSlide = slidesByIndex.get(index); if (activeSlide) measureFeedTitle(activeSlide); }; const onResize = function() { if (!state.feedOpen || state.feedActiveIndex < 0) return; + // Suppress scroll handling while we realign so the container's re-snap + // doesn't flip the active video, then re-enable it once layout settles. + suppressScroll = true; realignToActive(); // Orientation changes can settle over more than one frame (the visual // viewport and the scroll-snap re-anchor in stages); realign again once - // layout has settled so the active video stays put either way. - requestAnimationFrame(realignToActive); + // layout has settled, then stop suppressing real swipes. + if (resizeSettleRaf) cancelAnimationFrame(resizeSettleRaf); + resizeSettleRaf = requestAnimationFrame(() => { + realignToActive(); + resizeSettleRaf = requestAnimationFrame(() => { + resizeSettleRaf = null; + suppressScroll = false; + }); + }); }; App.feed.isOpen = function() { @@ -505,7 +509,11 @@ App.feed = App.feed || {}; resumeTimes.clear(); state.feedActiveIndex = -1; state.feedActiveVideoId = null; - lastSlideHeight = 0; + suppressScroll = false; + if (resizeSettleRaf) { + cancelAnimationFrame(resizeSettleRaf); + resizeSettleRaf = null; + } const spacer = getTopSpacer(); if (spacer) spacer.style.height = '0px'; const scroller = getScroller(); @@ -551,8 +559,7 @@ App.feed = App.feed || {}; // Force a fresh activation even if the index happens to match. state.feedActiveIndex = -1; setActive(startIndex); - lastSlideHeight = slideHeight(); - scroller.scrollTop = startIndex * lastSlideHeight; + scroller.scrollTop = startIndex * slideHeight(); App.feed.updateToggleButton(); App.feed.updateMuteButton();