fix(mobile): image pager direction + end-arrows; reset --kb on composer blur

- Pager: force the staged (opposite-edge) frame to PAINT (void offsetWidth) before animating in, so the
  incoming image enters from the correct side (was appearing from the exit side). Hide prev-arrow at the
  first image and next-arrow at the last.
- #1: keyboard closed after reply-cancel but the --kb lift stayed (empty gap). Add keyboardDidHide reset +
  a composer-blur fallback that drops --kb when focus leaves the composer.
Build marker -> 2026-07-18-batch119.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 23:11:05 +05:30
parent da3d44cbb7
commit 2bd8b3c8c1
+16 -10
View File
@@ -1134,7 +1134,7 @@
</head> </head>
<body> <body>
<script src="/icons.js?v=6"></script> <script src="/icons.js?v=6"></script>
<script>window.__BUILD='2026-07-18-batch118';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD); <script>window.__BUILD='2026-07-18-batch119';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
@@ -1156,10 +1156,12 @@ function twemojify(_el){ /* native emoji: nothing to do */ }</script>
// fires at the START of the animation, so the CSS bottom-transition slides the composer up WITH the // fires at the START of the animation, so the CSS bottom-transition slides the composer up WITH the
// keyboard (smooth). raw>innerHeight guards the rare device-pixel unit. // keyboard (smooth). raw>innerHeight guards the rare device-pixel unit.
function setKb(px){ px=Math.max(0,Math.round(px)); r.style.setProperty('--kb',px+'px'); document.body.classList.toggle('kb-open',px>4); var b=document.getElementById('msgs'); if(b) b.scrollTop=b.scrollHeight; } function setKb(px){ px=Math.max(0,Math.round(px)); r.style.setProperty('--kb',px+'px'); document.body.classList.toggle('kb-open',px>4); var b=document.getElementById('msgs'); if(b) b.scrollTop=b.scrollHeight; }
window.__bzResetKb=function(){ setKb(0); }; // called on composer blur as a safety net (#1: keyboard closed but lift stayed, leaving empty space after reply-cancel)
var KB=C.Plugins&&C.Plugins.Keyboard; var KB=C.Plugins&&C.Plugins.Keyboard;
if(KB && KB.addListener){ if(KB && KB.addListener){
KB.addListener('keyboardWillShow', function(info){ var raw=(info&&info.keyboardHeight)||0; var kb = raw>window.innerHeight ? raw/(window.devicePixelRatio||1) : raw; setKb(kb); if(window.bzDbg) bzDbg('kbShow',{raw:raw, applied:Math.round(kb)}); }); KB.addListener('keyboardWillShow', function(info){ var raw=(info&&info.keyboardHeight)||0; var kb = raw>window.innerHeight ? raw/(window.devicePixelRatio||1) : raw; setKb(kb); if(window.bzDbg) bzDbg('kbShow',{raw:raw, applied:Math.round(kb)}); });
KB.addListener('keyboardWillHide', function(){ setKb(0); }); KB.addListener('keyboardWillHide', function(){ setKb(0); });
KB.addListener('keyboardDidHide', function(){ setKb(0); }); // belt-and-suspenders: some close paths only fire Did*
} }
}catch(_){} })(); }catch(_){} })();
</script> </script>
@@ -2634,22 +2636,26 @@ function openLightbox(src){
},{passive:false}); },{passive:false});
img.addEventListener('touchend',()=>{ pinchD=0; }); img.addEventListener('touchend',()=>{ pinchD=0; });
const show=(i)=>{ if(!multi) return; idx=Math.max(0, Math.min(gallery.length-1, i)); const u=gallery[idx]; img.src=u; if(dl) dl.setAttribute('href',u); resetZoom(); }; // clamp (no wrap → no blank slide past the last) const syncArrows=()=>{ if(!multi) return; const pB=ov.querySelector('.lb-prev'), nB=ov.querySelector('.lb-next'); if(pB) pB.style.visibility=idx<=0?'hidden':'visible'; if(nB) nB.style.visibility=idx>=gallery.length-1?'hidden':'visible'; };
const show=(i)=>{ if(!multi) return; idx=Math.max(0, Math.min(gallery.length-1, i)); const u=gallery[idx]; img.src=u; if(dl) dl.setAttribute('href',u); resetZoom(); syncArrows(); }; // clamp (no wrap → no blank slide past the last) + hide the end arrow
const close=()=>{ ov.remove(); document.removeEventListener('keydown', onKey); }; const close=()=>{ ov.remove(); document.removeEventListener('keydown', onKey); };
// #8: animated pager — the current image slides fully out in the swipe direction, then the next/prev // #8: animated pager — the current image slides fully out in the swipe direction, then the next/prev
// image slides in from the OPPOSITE side (instead of an instant src swap). Stops at the first/last. // image slides in from the OPPOSITE side. Stops at the first/last.
const pagerGo=(dir)=>{ // dir:+1 next (out to the left), -1 prev (out to the right) const pagerGo=(dir)=>{ // dir:+1 next (current exits left, next enters from right), -1 prev (mirror)
const target = idx + (dir>0?1:-1); const target = idx + (dir>0?1:-1);
if(!multi || target<0 || target>=gallery.length){ img.style.transition='transform .18s'; img.style.transform=''; return; } // at an edge → spring back if(!multi || target<0 || target>=gallery.length){ img.style.transition='transform .18s'; img.style.transform=''; return; } // at an edge → spring back
const W=(window.innerWidth||400); let done=false; const W=(window.innerWidth||400); let done=false;
const finish=()=>{ if(done) return; done=true; img.removeEventListener('transitionend', finish); const finish=()=>{ if(done) return; done=true; img.removeEventListener('transitionend', finish);
show(target); // load the target image; resetZoom clears its transform show(target); // load the target image; resetZoom clears its transform
img.style.transition='none'; img.style.transform='translateX('+(dir>0?W:-W)+'px)'; // stage it off the opposite edge img.style.transition='none'; img.style.transform='translateX('+(dir>0?W:-W)+'px)'; // stage off the OPPOSITE edge (next=right, prev=left)
requestAnimationFrame(()=>{ img.style.transition='transform .2s cubic-bezier(.3,.72,.3,1)'; img.style.transform='translateX(0)'; setTimeout(()=>{ if(img){ img.style.transition=''; img.style.transform=''; } ov.style.background=''; }, 220); }); void img.offsetWidth; // force that staged position to PAINT, else the browser animates from the exit side (wrong direction)
img.style.transition='transform .22s cubic-bezier(.3,.72,.3,1)'; img.style.transform='translateX(0)';
setTimeout(()=>{ if(img){ img.style.transition=''; img.style.transform=''; } ov.style.background=''; }, 240);
}; };
img.style.transition='transform .16s ease-out'; img.style.transform='translateX('+(dir>0?-W:W)+'px)'; // current slides out img.style.transition='transform .16s ease-out'; img.style.transform='translateX('+(dir>0?-W:W)+'px)'; // current slides out (next→left, prev→right)
img.addEventListener('transitionend', finish); setTimeout(finish, 190); // fallback if transitionend misses img.addEventListener('transitionend', finish); setTimeout(finish, 180); // fallback if transitionend misses
}; };
syncArrows(); // initial: hide prev at first / next at last
// #8: swipe DOWN (when not zoomed) to dismiss, like native photo viewers — drag follows the finger and // #8: swipe DOWN (when not zoomed) to dismiss, like native photo viewers — drag follows the finger and
// fades the backdrop; release past a threshold closes, otherwise it springs back. // fades the backdrop; release past a threshold closes, otherwise it springs back.
let dsy=0, dsx=0, dDrag=false; let dsy=0, dsx=0, dDrag=false;
@@ -2775,7 +2781,7 @@ async function openConvo(kind,id){
const _d=getDraft(kind,id); if(_d){ inpEl.value=_d; autoGrow(inpEl); } // restore unsent draft const _d=getDraft(kind,id); if(_d){ inpEl.value=_d; autoGrow(inpEl); } // restore unsent draft
inpEl.addEventListener('paste', onPaste); inpEl.addEventListener('paste', onPaste);
inpEl.addEventListener('input', ()=>{ maybeAutocorrect(inpEl); autoGrow(inpEl); setDraft(kind,id,inpEl.value); noteTyping(); }); // auto-correct common typos, keep the draft in sync + emit "typing…" inpEl.addEventListener('input', ()=>{ maybeAutocorrect(inpEl); autoGrow(inpEl); setDraft(kind,id,inpEl.value); noteTyping(); }); // auto-correct common typos, keep the draft in sync + emit "typing…"
inpEl.addEventListener('blur', stopTyping); inpEl.addEventListener('blur', ()=>{ stopTyping(); setTimeout(()=>{ const a=document.activeElement; const stillTyping=a&&(a.tagName==='TEXTAREA'||a.tagName==='INPUT'); if(!stillTyping && window.__bzResetKb) window.__bzResetKb(); }, 120); }); // #1: composer lost focus → keyboard is closing → drop the --kb lift so no empty gap remains
inpEl.addEventListener('keydown', (e)=>{ if(e.key==='Enter' && !e.shiftKey){ if(mentionItems && mentionItems.length) return; e.preventDefault(); sendMessage(); } }); // Enter sends, Shift+Enter = newline inpEl.addEventListener('keydown', (e)=>{ if(e.key==='Enter' && !e.shiftKey){ if(mentionItems && mentionItems.length) return; e.preventDefault(); sendMessage(); } }); // Enter sends, Shift+Enter = newline
// Desktop: LEFT-click a red-squiggled word → native spelling suggestions right there (the shell // Desktop: LEFT-click a red-squiggled word → native spelling suggestions right there (the shell
// pops a suggestions menu only if the clicked word is actually misspelled). No right-click needed. // pops a suggestions menu only if the clicked word is actually misspelled). No right-click needed.