fix(mobile): revert keyboard to live VisualViewport tracking (no timer), undo budge; add unit test

- Keyboard: the 60ms timer in batch106 sampled the viewport MID-animation (clientHeight/visualViewport
  temporarily inconsistent) and over-lifted -> gap. Reverted to reading clientHeight-visualViewport LIVE
  on every vv resize, which is correct and self-adapting (unit test: residual=46 flush on the device).
- Budge: reverted the file <input> back to display:none (the position:fixed;left:50% hack overflowed
  the viewport horizontally).
- Config back to resize:native to match the installed build (works, no rebuild).
- Added test/keyboard-lift.test.js (5/5 pass) verifying the lift math against real device numbers.
Build marker -> 2026-07-18-batch107.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 10:22:45 +05:30
parent 6e3cf95deb
commit 720b1f0c40
3 changed files with 49 additions and 28 deletions
+1 -1
View File
@@ -17,7 +17,7 @@
"offsetForKeyboardInsetBug": false
},
"Keyboard": {
"resize": "none"
"resize": "native"
},
"SplashScreen": {
"launchShowDuration": 900,
+17 -27
View File
@@ -1112,7 +1112,7 @@
</head>
<body>
<script src="/icons.js?v=6"></script>
<script>window.__BUILD='2026-07-18-batch106';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
<script>window.__BUILD='2026-07-18-batch107';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,32 +1127,22 @@ 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 & 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'); });
// Keyboard — DEVICE-INDEPENDENT. The exact amount to lift the composer so it sits on the keyboard is
// the difference between the layout viewport and the VISIBLE viewport: clientHeight - visualViewport.
// This is self-adapting: with resize:native the WebView already shrank so the difference is the small
// prediction-bar residual; with resize:none the difference is the full keyboard. We read it LIVE from
// VisualViewport (never on a timer, which would sample a mid-animation, inconsistent state — that was
// the bug that left a gap). See bzKeyboardLift() in the unit test for the pure logic.
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);
}
// 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>
@@ -1902,7 +1892,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="position:fixed;left:50%;bottom:0;width:1px;height:1px;opacity:0;border:0;padding:0;pointer-events:none;">'
+ '<input type="file" id="fileInput" multiple style="display:none">'
+ '</form>'
+ '<div class="emoji-pop" id="emojiPop" style="display:none"></div>'
+ '<div class="mention-pop" id="mentionPop" style="display:none"></div>'
+31
View File
@@ -0,0 +1,31 @@
// Unit test for the mobile keyboard-lift logic (pure function, no DOM).
// The composer must sit exactly on top of the keyboard. The amount to lift it is the gap between the
// LAYOUT viewport (document.documentElement.clientHeight) and the VISIBLE viewport
// (window.visualViewport.height + offsetTop). This is self-adapting across Capacitor keyboard resize
// modes, and needs no per-device constant.
//
// Run: node test/keyboard-lift.test.js
function bzKeyboardLift(clientHeight, vvHeight, vvTop) {
return Math.max(0, Math.round(clientHeight - vvHeight - (vvTop || 0)));
}
// Cases use REAL numbers captured from the on-device debug readout (iPhone, dpr=3):
const cases = [
// [name, clientHeight, vvHeight, vvTop, expectedLift]
['keyboard closed (all equal) ', 926, 926, 0, 0],
['resize:native open (WebView shrank; residual)', 581, 535, 0, 46], // <- the device values you sent
['resize:none open (WebView full; full lift) ', 926, 581, 0, 345],
['with a visual-viewport offset ', 926, 560, 20, 346],
['guard: never negative ', 500, 600, 0, 0],
];
let pass = 0, fail = 0;
for (const [name, ch, vh, vt, expected] of cases) {
const got = bzKeyboardLift(ch, vh, vt);
const ok = got === expected;
console.log(`${ok ? 'PASS' : 'FAIL'} ${name} lift=${got}${ok ? '' : ` (expected ${expected})`}`);
ok ? pass++ : fail++;
}
console.log(`\n${pass} passed, ${fail} failed`);
process.exit(fail ? 1 : 0);