fix(mobile): mode-adaptive smooth keyboard + force-latest-scroll + bottom-anchored file input

- Keyboard: auto-detect resize mode. resize:none -> lift full keyboard height at willShow so the CSS
  transition slides the composer up WITH the keyboard (smooth, no iOS resize lag). resize:native ->
  residual only. Config set to resize:none for the smooth path (needs build); web stays correct on the
  current resize:native build meanwhile.
- #7: force scroll-to-bottom at 0/120/320/600ms on open so a chat always opens on the newest message.
- File picker: anchor the file <input> to a fixed bottom spot (was display:none -> iOS dropped the menu
  mid-screen); now it comes up as a bottom sheet even as the composer moves.
Build marker -> 2026-07-18-batch106.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 08:32:23 +05:30
parent c775ca2740
commit 6e3cf95deb
2 changed files with 33 additions and 17 deletions
+1 -1
View File
@@ -17,7 +17,7 @@
"offsetForKeyboardInsetBug": false "offsetForKeyboardInsetBug": false
}, },
"Keyboard": { "Keyboard": {
"resize": "native" "resize": "none"
}, },
"SplashScreen": { "SplashScreen": {
"launchShowDuration": 900, "launchShowDuration": 900,
+32 -16
View File
@@ -1072,7 +1072,7 @@
/* Composer clears the home indicator; with keyboard resize:none we raise it by --kb (the reported /* Composer clears the home indicator; with keyboard resize:none we raise it by --kb (the reported
keyboard height, set by the Keyboard listener) so it sits on the keyboard. No home indicator while keyboard height, set by the Keyboard listener) so it sits on the keyboard. No home indicator while
the keyboard is up, so --sab collapses to 0. */ the keyboard is up, so --sab collapses to 0. */
body.chat-open .content .panel{bottom:calc(var(--sab) + var(--kb));transition:bottom .18s ease;} body.chat-open .content .panel{bottom:calc(var(--sab) + var(--kb));transition:bottom .25s cubic-bezier(.17,.59,.4,1);}
body.kb-open{--sab:0px;} body.kb-open{--sab:0px;}
/* Native-style push/pop: the conversation slides in from the right when opened and slides back out /* Native-style push/pop: the conversation slides in from the right when opened and slides back out
when you go back, so navigation has a clear direction (like Teams/WhatsApp). */ when you go back, so navigation has a clear direction (like Teams/WhatsApp). */
@@ -1112,7 +1112,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-batch105';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD); <script>window.__BUILD='2026-07-18-batch106';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
@@ -1127,20 +1127,32 @@ function twemojify(_el){ /* native emoji: nothing to do */ }</script>
// juggling here — we only tag <html> so CSS can make small native-only tweaks if needed. // juggling here — we only tag <html> so CSS can make small native-only tweaks if needed.
(function(){ try{ var C=window.Capacitor; if(!C||!C.isNativePlatform||!C.isNativePlatform()) return; (function(){ try{ var C=window.Capacitor; if(!C||!C.isNativePlatform||!C.isNativePlatform()) return;
var p=(C.getPlatform&&C.getPlatform())||''; var r=document.documentElement; r.classList.add('native-app'); if(p) r.classList.add('native-'+p); var p=(C.getPlatform&&C.getPlatform())||''; var r=document.documentElement; r.classList.add('native-app'); if(p) r.classList.add('native-'+p);
// Keyboard — DEVICE-INDEPENDENT & self-adjusting. resize:native already shrinks the WebView by the // Keyboard — DEVICE-INDEPENDENT & mode-adaptive (works whether the WebView is resize:native or
// keyboard height, so the composer only needs the RESIDUAL lift = (layout viewport height - visible // resize:none). We lift the composer so it sits on the keyboard:
// viewport height) — the sliver still under the keyboard's prediction/accessory bar. Read live from // • resize:none → the WebView doesn't move, so lift by the FULL keyboard height (reported at
// the VisualViewport API so the composer follows the keyboard smoothly. No per-device constants. // keyboardWillShow, before any animation) → the CSS transition slides it up WITH the keyboard.
if(window.visualViewport){ // • resize:native→ the WebView already shrank by the keyboard height, so lift only the RESIDUAL
var vv=window.visualViewport; // (clientHeight visualViewport.height). Detected once by checking if clientHeight shrank.
var applyKb=function(){ // No per-device constants — the keyboard height comes from the plugin / VisualViewport.
var kb=Math.max(0, Math.round(document.documentElement.clientHeight - vv.height - vv.offsetTop)); function setKb(px){ px=Math.max(0,Math.round(px)); r.style.setProperty('--kb',px+'px'); var b=document.getElementById('msgs'); if(b) b.scrollTop=b.scrollHeight; }
r.style.setProperty('--kb', kb+'px'); var kbMode=null; var KB=C.Plugins&&C.Plugins.Keyboard;
document.body.classList.toggle('kb-open', kb>4); if(KB && KB.addListener){
var b=document.getElementById('msgs'); if(b) b.scrollTop=b.scrollHeight; KB.addListener('keyboardWillShow', function(info){
}; document.body.classList.add('kb-open');
vv.addEventListener('resize', applyKb); vv.addEventListener('scroll', applyKb); var raw=(info&&info.keyboardHeight)||0; var kbFull = raw>window.innerHeight ? raw/(window.devicePixelRatio||1) : raw; // normalize px→pt
var baseH=document.documentElement.clientHeight;
if(kbMode==='none') setKb(kbFull); // known non-resizing WebView → lift immediately (smooth)
setTimeout(function(){
var nowH=document.documentElement.clientHeight;
if(!kbMode) kbMode=(baseH-nowH>20)?'native':'none';
if(kbMode==='native'){ var vv=window.visualViewport; setKb(vv?Math.max(0, nowH - vv.height - vv.offsetTop):0); }
else setKb(kbFull);
}, 60);
});
KB.addListener('keyboardWillHide', function(){ r.style.setProperty('--kb','0px'); document.body.classList.remove('kb-open'); });
} }
// Live-track while the keyboard is up (covers rotation + resize:native's late resize) without doubling.
if(window.visualViewport){ window.visualViewport.addEventListener('resize', function(){ if(kbMode==='native' && document.body.classList.contains('kb-open')){ var vv=window.visualViewport; setKb(Math.max(0, document.documentElement.clientHeight - vv.height - vv.offsetTop)); } }); }
}catch(_){} })(); }catch(_){} })();
</script> </script>
<div class="loading" id="loading"><div class="ld-inner"><img src="/loaders/loader-orbit-dark.svg" alt="" width="72" height="72"><div class="ld-wm">Biz <b>Connect</b></div><span class="ld-sub">Loading…</span></div></div> <div class="loading" id="loading"><div class="ld-inner"><img src="/loaders/loader-orbit-dark.svg" alt="" width="72" height="72"><div class="ld-wm">Biz <b>Connect</b></div><span class="ld-sub">Loading…</span></div></div>
@@ -1890,7 +1902,7 @@ function convoShellHTML(it){
+ '<button type="submit" class="sendbtn" title="Send" aria-label="Send">'+ic('send',18)+'</button>' + '<button type="submit" class="sendbtn" title="Send" aria-label="Send">'+ic('send',18)+'</button>'
+ '</div>' + '</div>'
+ '</div>' + '</div>'
+ '<input type="file" id="fileInput" multiple style="display:none">' + '<input type="file" id="fileInput" multiple style="position:fixed;left:50%;bottom:0;width:1px;height:1px;opacity:0;border:0;padding:0;pointer-events:none;">'
+ '</form>' + '</form>'
+ '<div class="emoji-pop" id="emojiPop" style="display:none"></div>' + '<div class="emoji-pop" id="emojiPop" style="display:none"></div>'
+ '<div class="mention-pop" id="mentionPop" style="display:none"></div>' + '<div class="mention-pop" id="mentionPop" style="display:none"></div>'
@@ -2818,6 +2830,10 @@ async function openConvo(kind,id){
if(el){ const sep=document.createElement('div'); sep.className='new-sep'; sep.innerHTML='<span>New messages</span>'; el.parentNode.insertBefore(sep, el); el.scrollIntoView({block:'start'}); el.parentNode.scrollTop-=40; const jl=document.getElementById('jumpLatest'); if(jl) jl.style.display='grid'; } if(el){ const sep=document.createElement('div'); sep.className='new-sep'; sep.innerHTML='<span>New messages</span>'; el.parentNode.insertBefore(sep, el); el.scrollIntoView({block:'start'}); el.parentNode.scrollTop-=40; const jl=document.getElementById('jumpLatest'); if(jl) jl.style.display='grid'; }
} }
const inp=document.getElementById('msgInput'); if(inp && !isMobileUA()) inp.focus(); // #2: on phones, opening a chat should NOT pop the keyboard (user taps the box to type) const inp=document.getElementById('msgInput'); if(inp && !isMobileUA()) inp.focus(); // #2: on phones, opening a chat should NOT pop the keyboard (user taps the box to type)
// #7: guarantee the newest message is in view on open — force the scroll to the bottom a few times as
// late content (avatars, images, day separators) settles, unless we deliberately stopped at a "New
// messages" divider. This is the safety net over renderThread()'s own pin.
[0,120,320,600].forEach((d)=>setTimeout(()=>{ if(selected&&selected.kind===kind&&selected.id===id){ const b=document.getElementById('msgs'); if(b && !b.querySelector('.new-sep')) b.scrollTop=b.scrollHeight; } }, d));
} }
// ---------- @mentions (group chat) ---------- // ---------- @mentions (group chat) ----------
let mentionItems=[], mentionIdx=0, mentionStart=-1; let mentionItems=[], mentionIdx=0, mentionStart=-1;