mobile: stop the open-pin schedule the instant the user scrolls (batch131)
Bug: for ~3.4s after opening a chat, the "keep newest in view as images load" pin schedule would yank you back to the bottom if you tried to scroll up for older history — so scrolling up snapped back to latest, and only worked once the schedule expired (~5s). Fix: the user's first scroll gesture now sets _openScrolled, which cancels the pending pin timers and short- circuits _pinNewest (so the late image-load pins stop too). loadOlder/anchor then works immediately, no yank. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1146,7 +1146,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script src="/icons.js?v=6"></script>
|
<script src="/icons.js?v=6"></script>
|
||||||
<script>window.__BUILD='2026-07-19-batch130';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
|
<script>window.__BUILD='2026-07-19-batch131';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
|
||||||
// Emoji are rendered with the OS's own (colour) emoji font — instant, zero network.
|
// Emoji are rendered with the OS's own (colour) emoji font — instant, zero network.
|
||||||
//
|
//
|
||||||
// We used to run Twemoji over every emoji, which swapped each one for an <img> pulled INDIVIDUALLY from
|
// We used to run Twemoji over every emoji, which swapped each one for an <img> pulled INDIVIDUALLY from
|
||||||
@@ -2850,6 +2850,7 @@ async function openConvo(kind,id){
|
|||||||
const _unreadN=_openUnread; _openUnread=0; // consume the captured unread count (#3)
|
const _unreadN=_openUnread; _openUnread=0; // consume the captured unread count (#3)
|
||||||
_convoRevealed=false; // #7: this open hasn't revealed yet
|
_convoRevealed=false; // #7: this open hasn't revealed yet
|
||||||
_forcePinOpen=true; // glue to newest until the user actually scrolls (cleared on the first touch/wheel below)
|
_forcePinOpen=true; // glue to newest until the user actually scrolls (cleared on the first touch/wheel below)
|
||||||
|
let _openScrolled=false, _openPinTimers=[]; // the moment the user scrolls we STOP the open-pin schedule so it can't yank them back off older history they're trying to reach
|
||||||
convoIsGroup=(kind==='group');
|
convoIsGroup=(kind==='group');
|
||||||
const el=document.getElementById('chatPanel'); el.classList.remove('center');
|
const el=document.getElementById('chatPanel'); el.classList.remove('center');
|
||||||
el.innerHTML=convoShellHTML(it);
|
el.innerHTML=convoShellHTML(it);
|
||||||
@@ -2938,7 +2939,7 @@ async function openConvo(kind,id){
|
|||||||
if(bub && !already && !bub.classList.contains('deleted')) bub.classList.add('show-actions');
|
if(bub && !already && !bub.classList.contains('deleted')) bub.classList.add('show-actions');
|
||||||
});
|
});
|
||||||
if(box) box.addEventListener('scroll', onMsgsScroll);
|
if(box) box.addEventListener('scroll', onMsgsScroll);
|
||||||
if(box){ const _rel=()=>{ _forcePinOpen=false; }; box.addEventListener('touchmove', _rel, {passive:true}); box.addEventListener('wheel', _rel, {passive:true}); } // the user's first real scroll gesture hands control back (stop force-gluing to the newest)
|
if(box){ const _rel=()=>{ _forcePinOpen=false; _openScrolled=true; _openPinTimers.forEach(clearTimeout); _openPinTimers.length=0; }; box.addEventListener('touchmove', _rel, {passive:true}); box.addEventListener('wheel', _rel, {passive:true}); } // the user's first real scroll gesture hands control back: stop force-gluing to the newest AND cancel the pending open-pins so scrolling up for older history isn't yanked back down
|
||||||
if(box) enablePullRefresh(box, loadOlder); // pull down at the top loads OLDER history (or no-ops); never jumps to the newest/bottom
|
if(box) enablePullRefresh(box, loadOlder); // pull down at the top loads OLDER history (or no-ops); never jumps to the newest/bottom
|
||||||
const jl=document.getElementById('jumpLatest'); if(jl) jl.onclick=()=>{ const b=document.getElementById('msgs'); if(b) b.scrollTop=b.scrollHeight; };
|
const jl=document.getElementById('jumpLatest'); if(jl) jl.onclick=()=>{ const b=document.getElementById('msgs'); if(b) b.scrollTop=b.scrollHeight; };
|
||||||
composeMentions=new Map(); convoMembers=[];
|
composeMentions=new Map(); convoMembers=[];
|
||||||
@@ -3000,11 +3001,11 @@ async function openConvo(kind,id){
|
|||||||
// #7: land on the newest message AND stay there while late content (images/avatars in long threads)
|
// #7: land on the newest message AND stay there while late content (images/avatars in long threads)
|
||||||
// keeps growing the thread after the first render. Re-pin on a schedule and on every image load, but
|
// keeps growing the thread after the first render. Re-pin on a schedule and on every image load, but
|
||||||
// only while still near the bottom, so a user who deliberately scrolled up isn't yanked back down.
|
// only while still near the bottom, so a user who deliberately scrolled up isn't yanked back down.
|
||||||
const _pinNewest=()=>{ if(!(selected&&selected.kind===kind&&selected.id===id)) return; const b=document.getElementById('msgs'); if(!b||b.querySelector('.new-sep')) return; if(_forcePinOpen || (b.scrollHeight-b.scrollTop-b.clientHeight)<1200) b.scrollTop=b.scrollHeight; }; // _forcePinOpen: on a fresh open, force the newest into view no matter how far late images push it, until the user scrolls
|
const _pinNewest=()=>{ if(_openScrolled) return; if(!(selected&&selected.kind===kind&&selected.id===id)) return; const b=document.getElementById('msgs'); if(!b||b.querySelector('.new-sep')) return; if(_forcePinOpen || (b.scrollHeight-b.scrollTop-b.clientHeight)<1200) b.scrollTop=b.scrollHeight; }; // stops the instant the user scrolls (_openScrolled); until then _forcePinOpen forces the newest into view as late images load
|
||||||
// Re-glue to the newest as late images/day-separators grow the thread. Spread over time (the compositor fade
|
// Re-glue to the newest as late images/day-separators grow the thread. Spread over time (the compositor fade
|
||||||
// is unaffected by these main-thread pins), so "open at latest" holds while images finish loading.
|
// is unaffected by these main-thread pins), so "open at latest" holds while images finish loading.
|
||||||
[0,120,320,600,1000,1600,2400,3400].forEach((d)=>setTimeout(_pinNewest, d));
|
if(!_openScrolled) _openPinTimers=[0,120,320,600,1000,1600,2400,3400].map((d)=>setTimeout(_pinNewest, d)); // cancellable: the user's first scroll clears these so an early scroll-up for older history isn't yanked back
|
||||||
{ const _pinBox=document.getElementById('msgs'); if(_pinBox){ _pinBox.querySelectorAll('img').forEach((im)=>{ if(!im.complete) im.addEventListener('load', _pinNewest, {once:true}); }); } }
|
{ const _pinBox=document.getElementById('msgs'); if(_pinBox){ _pinBox.querySelectorAll('img').forEach((im)=>{ if(!im.complete) im.addEventListener('load', _pinNewest, {once:true}); }); } } // _pinNewest self-guards on _openScrolled, so these late image-load pins also stop once the user scrolls
|
||||||
}
|
}
|
||||||
// ---------- @mentions (group chat) ----------
|
// ---------- @mentions (group chat) ----------
|
||||||
let mentionItems=[], mentionIdx=0, mentionStart=-1;
|
let mentionItems=[], mentionIdx=0, mentionStart=-1;
|
||||||
|
|||||||
Reference in New Issue
Block a user