Compare commits
2 Commits
872786a657
...
c60f5e01dc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c60f5e01dc | ||
|
|
7033288bc4 |
@@ -1413,16 +1413,40 @@ App.feed = App.feed || {};
|
|||||||
// video playing/focused rather than snapping to a neighbour.
|
// video playing/focused rather than snapping to a neighbour.
|
||||||
const realignToActive = function() {
|
const realignToActive = function() {
|
||||||
if (stepCount() === 0) return;
|
if (stepCount() === 0) return;
|
||||||
|
const h = slideHeight();
|
||||||
|
const scroller = getScroller();
|
||||||
|
// Anchoring by id is what keeps a rotation on the same video, because
|
||||||
|
// the scroll position it would otherwise read has been invalidated by
|
||||||
|
// the very change being answered. But mid-swipe that id names the video
|
||||||
|
// being *left*: setActive runs from onScroll's next frame, so a resize
|
||||||
|
// landing before it still reads the old one -- and writing its position
|
||||||
|
// hauls the reel back to it. A phone slides its chrome on every scroll,
|
||||||
|
// so that happened on nearly every swipe.
|
||||||
|
//
|
||||||
|
// While the reader is demonstrably driving, the reel's own position is
|
||||||
|
// the better authority: re-anchor on the step it is nearest at the new
|
||||||
|
// height. That still puts it on a snap point -- no slide left straddling
|
||||||
|
// two screens -- without undoing the swipe.
|
||||||
let index = state.feedActiveIndex;
|
let index = state.feedActiveIndex;
|
||||||
if (state.feedActiveVideoId != null) {
|
if (state.feedActiveVideoId != null) {
|
||||||
const found = (state.loadedVideos || [])
|
const found = (state.loadedVideos || [])
|
||||||
.findIndex((v) => String(v.id) === String(state.feedActiveVideoId));
|
.findIndex((v) => String(v.id) === String(state.feedActiveVideoId));
|
||||||
if (found >= 0) index = Math.floor(found / paneCount());
|
if (found >= 0) index = Math.floor(found / paneCount());
|
||||||
}
|
}
|
||||||
|
const driving = Date.now() - lastInputAt < INPUT_GRACE_MS;
|
||||||
|
if (driving && scroller && h) {
|
||||||
|
// A swipe moves by a step, so only a step's worth of disagreement
|
||||||
|
// is the reader's doing. Anything further apart is the position
|
||||||
|
// being meaningless rather than ahead -- a scroller that has just
|
||||||
|
// been re-parented into (or out of) the picture-in-picture window
|
||||||
|
// reads 0, and following that would land the reel on the first
|
||||||
|
// video instead of theirs.
|
||||||
|
const atScroll = clampIndex(Math.round(scroller.scrollTop / h));
|
||||||
|
if (atScroll >= 0 && Math.abs(atScroll - index) <= 1) index = atScroll;
|
||||||
|
}
|
||||||
index = clampIndex(index);
|
index = clampIndex(index);
|
||||||
if (index < 0) return;
|
if (index < 0) return;
|
||||||
state.feedActiveIndex = index;
|
state.feedActiveIndex = index;
|
||||||
const h = slideHeight();
|
|
||||||
const start = windowBounds(index).start;
|
const start = windowBounds(index).start;
|
||||||
// Only write what actually differs. This runs on every frame of a
|
// Only write what actually differs. This runs on every frame of a
|
||||||
// turn, and a spacer height or a scrollTop written back unchanged
|
// turn, and a spacer height or a scrollTop written back unchanged
|
||||||
@@ -1431,7 +1455,6 @@ App.feed = App.feed || {};
|
|||||||
const spacer = getTopSpacer();
|
const spacer = getTopSpacer();
|
||||||
const spacerHeight = `${start * h}px`;
|
const spacerHeight = `${start * h}px`;
|
||||||
if (spacer && spacer.style.height !== spacerHeight) spacer.style.height = spacerHeight;
|
if (spacer && spacer.style.height !== spacerHeight) spacer.style.height = spacerHeight;
|
||||||
const scroller = getScroller();
|
|
||||||
const top = index * h;
|
const top = index * h;
|
||||||
if (scroller && Math.abs(scroller.scrollTop - top) > 0.5) scroller.scrollTop = top;
|
if (scroller && Math.abs(scroller.scrollTop - top) > 0.5) scroller.scrollTop = top;
|
||||||
const activeSlide = slidesByIndex.get(index);
|
const activeSlide = slidesByIndex.get(index);
|
||||||
@@ -1492,15 +1515,29 @@ App.feed = App.feed || {};
|
|||||||
if (!turnPin && !turnResolved && isTurn(settleFromShape, shape)) {
|
if (!turnPin && !turnResolved && isTurn(settleFromShape, shape)) {
|
||||||
pinTurn(settleFromShape);
|
pinTurn(settleFromShape);
|
||||||
}
|
}
|
||||||
|
// A turn the device announced is not over because the viewport paused.
|
||||||
|
// It arrives in steps, and the gap between two of them is routinely
|
||||||
|
// longer than the handful of frames that stands in for "stopped" --
|
||||||
|
// which is how the pin came off mid-turn and let the picture take an
|
||||||
|
// intermediate shape, the one thing it is here to prevent. So while
|
||||||
|
// the announcement stands the pin holds, and the frame count is left
|
||||||
|
// to end the pins nothing announced: a window dragged across the
|
||||||
|
// square, where there is no signal to go on and no turn to ruin.
|
||||||
|
const announced = Date.now() - turnSignalAt < TURN_SIGNAL_MS;
|
||||||
// The viewport has stopped moving, so what it is now is better than
|
// The viewport has stopped moving, so what it is now is better than
|
||||||
// what the pin guessed it would be. Letting go here, with the realign
|
// what the pin guessed it would be. Letting go here, with the realign
|
||||||
// below in the same frame, is the one step the picture takes.
|
// below in the same frame, is the one step the picture takes.
|
||||||
if (turnPin && settleStableFrames >= TURN_HOLD_FRAMES) {
|
if (turnPin && !announced && settleStableFrames >= TURN_HOLD_FRAMES) {
|
||||||
releaseTurn();
|
releaseTurn();
|
||||||
turnResolved = true;
|
turnResolved = true;
|
||||||
}
|
}
|
||||||
realignToActive();
|
realignToActive();
|
||||||
if (settleStableFrames < SETTLE_STABLE_FRAMES &&
|
// The window may not conclude mid-turn either: closing it releases the
|
||||||
|
// pin on the way out, which would put the picture through the same
|
||||||
|
// intermediate shape by the other door. An announced turn therefore
|
||||||
|
// holds the window open too, and SETTLE_MAX_MS -- longer than the
|
||||||
|
// announcement -- is what still guarantees it ends.
|
||||||
|
if ((announced || settleStableFrames < SETTLE_STABLE_FRAMES) &&
|
||||||
(Date.now() - settleStartedAt) < SETTLE_MAX_MS) {
|
(Date.now() - settleStartedAt) < SETTLE_MAX_MS) {
|
||||||
resizeSettleRaf = requestAnimationFrame(settleStep);
|
resizeSettleRaf = requestAnimationFrame(settleStep);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -90,6 +90,22 @@ RESNAP = """(top) => {
|
|||||||
document.getElementById('feed-scroll').scrollTop = top;
|
document.getElementById('feed-scroll').scrollTop = top;
|
||||||
}"""
|
}"""
|
||||||
|
|
||||||
|
# A swipe, and the chrome reporting itself before the feed has caught up.
|
||||||
|
# setActive runs from onScroll's next frame, so doing both in one block --
|
||||||
|
# no frame boundary in between -- leaves feedActiveVideoId still naming the
|
||||||
|
# video being left, which is the state a phone resize routinely lands in.
|
||||||
|
# onResize then re-anchors synchronously, with no scroll event for the
|
||||||
|
# input guard in onScroll to see.
|
||||||
|
SWIPE_THEN_RESIZE = """() => {
|
||||||
|
const s = document.getElementById('feed-scroll');
|
||||||
|
const before = App.state.feedActiveIndex;
|
||||||
|
s.dispatchEvent(new WheelEvent('wheel', { deltaY: 120, bubbles: true }));
|
||||||
|
s.scrollTop = s.scrollTop + s.clientHeight;
|
||||||
|
const moved = s.scrollTop;
|
||||||
|
window.dispatchEvent(new Event('resize'));
|
||||||
|
return { before: before, moved: moved, after: s.scrollTop, h: s.clientHeight };
|
||||||
|
}"""
|
||||||
|
|
||||||
|
|
||||||
class Checks:
|
class Checks:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -187,6 +203,22 @@ def main():
|
|||||||
c.ok("re-anchored onto its own snap point",
|
c.ok("re-anchored onto its own snap point",
|
||||||
abs(resnapped["scrollTop"] - resnapped["expected"]) <= 1, str(resnapped))
|
abs(resnapped["scrollTop"] - resnapped["expected"]) <= 1, str(resnapped))
|
||||||
|
|
||||||
|
print("\nthe chrome reporting itself before the feed has caught up")
|
||||||
|
page.wait_for_timeout(900)
|
||||||
|
pre = page.evaluate(WHERE)
|
||||||
|
got = page.evaluate(SWIPE_THEN_RESIZE)
|
||||||
|
page.wait_for_timeout(2200)
|
||||||
|
kept = page.evaluate(WHERE)
|
||||||
|
print(f" (swiped to {got['moved']}px; the resize left it at {got['after']}px)")
|
||||||
|
c.ok("the resize did not haul the reel back to the video being left",
|
||||||
|
got["after"] >= got["moved"] - 1,
|
||||||
|
f"{got['moved']} -> {got['after']}")
|
||||||
|
c.ok("the swipe still landed on the next step",
|
||||||
|
kept["index"] == pre["index"] + 1,
|
||||||
|
f"step {pre['index']} -> {kept['index']}")
|
||||||
|
c.ok("and on that step's snap point",
|
||||||
|
abs(kept["scrollTop"] - kept["expected"]) <= 1, str(kept))
|
||||||
|
|
||||||
print("\na little height on a near-square viewport is not a rotation")
|
print("\na little height on a near-square viewport is not a rotation")
|
||||||
page.set_viewport_size(SQUARE)
|
page.set_viewport_size(SQUARE)
|
||||||
page.wait_for_timeout(1800)
|
page.wait_for_timeout(1800)
|
||||||
@@ -202,8 +234,8 @@ def main():
|
|||||||
c.ok("and the reel rests on its snap point",
|
c.ok("and the reel rests on its snap point",
|
||||||
abs(settled["scrollTop"] - settled["expected"]) <= 1, str(settled))
|
abs(settled["scrollTop"] - settled["expected"]) <= 1, str(settled))
|
||||||
c.ok("still on the same video",
|
c.ok("still on the same video",
|
||||||
settled["videoId"] == resnapped["videoId"],
|
settled["videoId"] == kept["videoId"],
|
||||||
f"{resnapped['videoId']} -> {settled['videoId']}")
|
f"{kept['videoId']} -> {settled['videoId']}")
|
||||||
|
|
||||||
c.ok("nothing threw along the way", not crashes, str(crashes[:2]))
|
c.ok("nothing threw along the way", not crashes, str(crashes[:2]))
|
||||||
browser.close()
|
browser.close()
|
||||||
|
|||||||
Reference in New Issue
Block a user