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
},
"Keyboard": {
"resize": "native"
"resize": "none"
},
"SplashScreen": {
"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
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. */
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;}
/* 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). */
@@ -1112,7 +1112,7 @@
</head>
<body>
<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.
//
// 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.
(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);
// Keyboard — DEVICE-INDEPENDENT & self-adjusting. resize:native already shrinks the WebView by the
// keyboard height, so the composer only needs the RESIDUAL lift = (layout viewport height - visible
// viewport height) — the sliver still under the keyboard's prediction/accessory bar. Read live from
// the VisualViewport API so the composer follows the keyboard smoothly. No per-device constants.
if(window.visualViewport){
var vv=window.visualViewport;
var applyKb=function(){
var kb=Math.max(0, Math.round(document.documentElement.clientHeight - vv.height - vv.offsetTop));
r.style.setProperty('--kb', kb+'px');
document.body.classList.toggle('kb-open', kb>4);
var b=document.getElementById('msgs'); if(b) b.scrollTop=b.scrollHeight;
};
vv.addEventListener('resize', applyKb); vv.addEventListener('scroll', applyKb);
// Keyboard — DEVICE-INDEPENDENT & mode-adaptive (works whether the WebView is resize:native or
// resize:none). We lift the composer so it sits on the keyboard:
// • resize:none → the WebView doesn't move, so lift by the FULL keyboard height (reported at
// keyboardWillShow, before any animation) → the CSS transition slides it up WITH the keyboard.
// • resize:native→ the WebView already shrank by the keyboard height, so lift only the RESIDUAL
// (clientHeight visualViewport.height). Detected once by checking if clientHeight shrank.
// No per-device constants — the keyboard height comes from the plugin / VisualViewport.
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; }
var kbMode=null; var KB=C.Plugins&&C.Plugins.Keyboard;
if(KB && KB.addListener){
KB.addListener('keyboardWillShow', function(info){
document.body.classList.add('kb-open');
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(_){} })();
</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>
@@ -1890,7 +1902,7 @@ function convoShellHTML(it){
+ '<button type="submit" class="sendbtn" title="Send" aria-label="Send">'+ic('send',18)+'</button>'
+ '</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>'
+ '<div class="emoji-pop" id="emojiPop" 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'; }
}
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) ----------
let mentionItems=[], mentionIdx=0, mentionStart=-1;