fix(ui): smooth iPhone PWA bottom-edge bounce in chat

This commit is contained in:
dobby-d-elf
2026-05-11 22:08:32 -06:00
parent 306dd2bf09
commit ff0830de4d
2 changed files with 54 additions and 1 deletions
+32 -1
View File
@@ -1657,6 +1657,36 @@ let _messageUserUnpinned=false;
let _bottomSettleToken=0;
const NON_MESSAGE_SCROLL_INTENT_SUPPRESS_MS=350;
const MESSAGE_UPWARD_INTENT_MS=450;
function _isIosStandalonePwa(){
try{
const ua=navigator.userAgent||'';
const isIosDevice=/iP(?:hone|ad|od)/i.test(ua)
|| (navigator.platform==='MacIntel'&&navigator.maxTouchPoints>1);
if(!isIosDevice) return false;
const standalone=(typeof navigator!=='undefined'&&navigator.standalone===true)
|| (typeof window.matchMedia==='function'&&window.matchMedia('(display-mode: standalone)').matches)
|| (typeof window.matchMedia==='function'&&window.matchMedia('(display-mode: fullscreen)').matches);
if(!standalone) return false;
return typeof window.matchMedia!=='function' || window.matchMedia('(pointer: coarse)').matches;
}catch(_){
return false;
}
}
function _messagePanePreferredBottomScrollTop(el){
if(!el) return 0;
const maxTop=Math.max(0,el.scrollHeight-el.clientHeight);
if(maxTop<=1) return maxTop;
return _isIosStandalonePwa()?maxTop-1:maxTop;
}
function _maybeInsetIosStandaloneBottomEdge(el, top){
if(!el||!_isIosStandalonePwa()) return;
const preferredTop=_messagePanePreferredBottomScrollTop(el);
if(preferredTop<=0||top<el.scrollHeight-el.clientHeight) return;
_programmaticScroll=true;
el.scrollTop=preferredTop;
_lastScrollTop=el.scrollTop;
requestAnimationFrame(()=>{ setTimeout(()=>{_programmaticScroll=false;},0); });
}
function _cancelBottomSettle(){ _bottomSettleToken++; }
function _recordNonMessageScrollIntent(e){
const el=document.getElementById('messages');
@@ -1716,6 +1746,7 @@ if (typeof window !== 'undefined') window._resetScrollDirectionTracker = _resetS
} else { _nearBottomCount=0; _scrollPinned=false; }
if(_scrollPinned) _messageUserUnpinned=false;
} // #1360
_maybeInsetIosStandaloneBottomEdge(el, top);
const btn=$('scrollToBottomBtn');
if(btn) btn.style.display=_scrollPinned?'none':'flex';
if(typeof _updateSessionStartJumpButton==='function') _updateSessionStartJumpButton();
@@ -2009,7 +2040,7 @@ function _setMessageScrollToBottom(){
const el=$('messages');
if(!el) return;
_programmaticScroll=true;
el.scrollTop=el.scrollHeight;
el.scrollTop=_messagePanePreferredBottomScrollTop(el);
_lastScrollTop=el.scrollTop;
_nearBottomCount=2;
_scrollPinned=true;
@@ -0,0 +1,22 @@
from pathlib import Path
REPO = Path(__file__).parent.parent
UI_JS = (REPO / 'static' / 'ui.js').read_text(encoding='utf-8')
def test_ios_standalone_detection_helper_exists():
assert 'function _isIosStandalonePwa()' in UI_JS
assert "window.matchMedia('(display-mode: standalone)').matches" in UI_JS
assert 'navigator.standalone===true' in UI_JS
def test_message_bottom_prefers_one_pixel_inset_on_ios_pwa():
assert 'function _messagePanePreferredBottomScrollTop(el)' in UI_JS
assert 'return _isIosStandalonePwa()?maxTop-1:maxTop;' in UI_JS
assert 'el.scrollTop=_messagePanePreferredBottomScrollTop(el);' in UI_JS
def test_ios_pwa_bottom_edge_guard_installed_on_messages_pane():
assert 'function _maybeInsetIosStandaloneBottomEdge(el, top)' in UI_JS
assert 'if(preferredTop<=0||top<el.scrollHeight-el.clientHeight) return;' in UI_JS
assert '_maybeInsetIosStandaloneBottomEdge(el, top);' in UI_JS