fix(#5544): re-pin auto-scroll only at true bottom after scroll-to-bottom
A single scroll-up during streaming set _messageUserUnpinned=true and scrollIfPinned() then permanently stopped auto-follow (only scrollToBottom() cleared it) — a permanent auto-follow lockout even after the user returned to the bottom. scrollIfPinned() now re-pins, but ONLY when the reader has genuinely reached the true-bottom tail (<=80px) AND shows no active scroll intent (wheel/key/touch/non-message), reusing the listener's _nearBottomCount debounce. Proximity alone (the ~250px nearBottom band) must never re-pin — that is the #4295 invariant. Restores the listener's <=80px true-bottom gate too. Co-authored-by: luperrypf <luperrypf@users.noreply.github.com>
This commit is contained in:
1 parent
627c6a173c
commit
4dc87e9069
3 files changed
+47
-4
No files matched your search
+22
-1
@@ -4858,6 +4858,10 @@ if(typeof window!=='undefined'){
|
|||||||
}else if(movedDown&&nearBottom){
|
}else if(movedDown&&nearBottom){
|
||||||
_nearBottomCount=_nearBottomCount+1;
|
_nearBottomCount=_nearBottomCount+1;
|
||||||
if(_nearBottomCount>=2){
|
if(_nearBottomCount>=2){
|
||||||
|
// Only re-pin when the reader has genuinely reached the true bottom
|
||||||
|
// tail (<=80px). nearBottom spans a ~250px band, so proximity alone
|
||||||
|
// must NOT clear the sticky unpin flag (#4295) — a reader scanning the
|
||||||
|
// last lines mid-stream would otherwise get yanked back to the bottom.
|
||||||
if(!_messageUserUnpinned||bottomDistance<=80){
|
if(!_messageUserUnpinned||bottomDistance<=80){
|
||||||
_messageUserUnpinned=false;
|
_messageUserUnpinned=false;
|
||||||
_scrollPinned=true;
|
_scrollPinned=true;
|
||||||
@@ -5516,7 +5520,24 @@ function _settleFinalScroll(token){
|
|||||||
}
|
}
|
||||||
function scrollIfPinned(){
|
function scrollIfPinned(){
|
||||||
if(!_autoScrollFollow) return;
|
if(!_autoScrollFollow) return;
|
||||||
if(_messageUserUnpinned) return;
|
if(_messageUserUnpinned){
|
||||||
|
// Only scrollToBottom() cleared this flag, so one scroll-up permanently
|
||||||
|
// killed auto-follow. Re-pin ONLY when the reader has genuinely returned to
|
||||||
|
// the true bottom tail (<=80px), NOT on mere near-bottom proximity — the
|
||||||
|
// #4295 invariant is that proximity alone (inside the ~250px band) must not
|
||||||
|
// re-pin, or a reader scanning the last few lines gets yanked to the bottom
|
||||||
|
// mid-stream. Also bail on ANY recent message-pane scroll intent (wheel,
|
||||||
|
// key, touch) and non-message intent, so an active scroll-up near the tail
|
||||||
|
// is never overridden. Uses the same _nearBottomCount debounce as the
|
||||||
|
// scroll listener (~4859-4866).
|
||||||
|
if(_recentNonMessageScrollIntent()||_recentMessageScrollIntent()||_recentMessageTouchScrollIntent()||_recentMessageWheelIntent()||_recentMessageKeyScrollIntent()){ _nearBottomCount=0; return; }
|
||||||
|
if(_messageBottomDistance()>80){ _nearBottomCount=0; return; }
|
||||||
|
_nearBottomCount=_nearBottomCount+1;
|
||||||
|
if(_nearBottomCount<2) return;
|
||||||
|
_nearBottomCount=0;
|
||||||
|
_messageUserUnpinned=false;
|
||||||
|
_scrollPinned=true;
|
||||||
|
}
|
||||||
if(!_scrollPinned) return;
|
if(!_scrollPinned) return;
|
||||||
if(_recentNonMessageScrollIntent()) return;
|
if(_recentNonMessageScrollIntent()) return;
|
||||||
if(_messageBottomDistance()>500) _setMessageScrollToBottom();
|
if(_messageBottomDistance()>500) _setMessageScrollToBottom();
|
||||||
|
|||||||
@@ -32,8 +32,19 @@ def _scroll_listener_block() -> str:
|
|||||||
def test_scroll_if_pinned_respects_sticky_user_unpin():
|
def test_scroll_if_pinned_respects_sticky_user_unpin():
|
||||||
fn = UI_JS[UI_JS.index("function scrollIfPinned"): UI_JS.index("function scrollToBottom")]
|
fn = UI_JS[UI_JS.index("function scrollIfPinned"): UI_JS.index("function scrollToBottom")]
|
||||||
compact = fn.replace(" ", "")
|
compact = fn.replace(" ", "")
|
||||||
assert "if(_messageUserUnpinned)return" in compact, (
|
# scrollIfPinned() re-pins auto-follow when the reader returns to the tail
|
||||||
"scrollIfPinned() must not fight a sticky manual unpin during streaming"
|
# (#5544), but it must still RESPECT a sticky manual unpin: the re-pin is
|
||||||
|
# gated behind the true-bottom threshold (<=80px, not mere proximity) and
|
||||||
|
# bails on any recent scroll intent, so it never fights a reader who has
|
||||||
|
# scrolled up. Assert the guards rather than the old unconditional bail.
|
||||||
|
assert "if(_messageUserUnpinned){" in compact, (
|
||||||
|
"scrollIfPinned() must handle the sticky-unpin case explicitly"
|
||||||
|
)
|
||||||
|
assert "_messageBottomDistance()>80" in compact, (
|
||||||
|
"re-pin must require the true bottom tail (<=80px), not proximity — the #4295 invariant"
|
||||||
|
)
|
||||||
|
assert "_recentMessageWheelIntent()" in compact and "_recentMessageKeyScrollIntent()" in compact, (
|
||||||
|
"re-pin must bail on recent wheel/key scroll intent so it can't fight a manual scroll-up"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -100,7 +100,18 @@ def test_user_scroll_cancels_delayed_bottom_settling():
|
|||||||
assert "_cancelBottomSettle();" in record
|
assert "_cancelBottomSettle();" in record
|
||||||
assert "_lastNonMessageScrollIntentMs=performance.now();" in record
|
assert "_lastNonMessageScrollIntentMs=performance.now();" in record
|
||||||
assert "_scrollPinned=false" in record
|
assert "_scrollPinned=false" in record
|
||||||
assert "if(_messageUserUnpinned) return;" in pinned
|
# scrollIfPinned() may now re-pin auto-follow (#5544), but ONLY when the
|
||||||
|
# reader has genuinely returned to the true bottom tail (<=80px) AND no
|
||||||
|
# recent scroll intent is active — it must never fight a manual scroll-up.
|
||||||
|
# Assert those guards are present instead of the old "bail unconditionally
|
||||||
|
# when unpinned" lock.
|
||||||
|
_pinned_compact = pinned.replace(" ", "")
|
||||||
|
assert "_messageBottomDistance()>80" in _pinned_compact, (
|
||||||
|
"scrollIfPinned() re-pin must require the true bottom tail (<=80px), not mere proximity"
|
||||||
|
)
|
||||||
|
assert "_recentMessageWheelIntent()" in _pinned_compact and "_recentMessageKeyScrollIntent()" in _pinned_compact, (
|
||||||
|
"scrollIfPinned() re-pin must bail on recent wheel/key scroll intent so it can't fight a reader"
|
||||||
|
)
|
||||||
assert "_messageUserUnpinned" in final and "return" in final
|
assert "_messageUserUnpinned" in final and "return" in final
|
||||||
assert "_recentMessageUpwardIntent()" not in pinned
|
assert "_recentMessageUpwardIntent()" not in pinned
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user