fix(mobile): native keyboard (remove JS hack), open at latest + no double-render dance, splash cfg
- #3 keyboard: the overshoot proved resize:native DOES lift the composer; my --kb JS was a second lift. Removed the JS hack entirely; keyboard is now purely @capacitor/keyboard resize:native. Config: turn OFF safe-area offsetForKeyboardInsetBug so only one mechanism moves the view (needs the build). - #7: gate the open-time hide/reveal with _convoRevealed so the cache->network double render doesn't flash/dance; on mobile open at the LATEST message (skip auto-scroll to first-unread). - #6: add SplashScreen config (bg #16294F, no spinner) — asset gen already produces the logo. Build marker -> 2026-07-18-batch97. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -14,10 +14,17 @@
|
||||
"SafeArea": {
|
||||
"detectViewportFitCoverChanges": true,
|
||||
"initialViewportFitCover": true,
|
||||
"offsetForKeyboardInsetBug": true
|
||||
"offsetForKeyboardInsetBug": false
|
||||
},
|
||||
"Keyboard": {
|
||||
"resize": "none"
|
||||
"resize": "native"
|
||||
},
|
||||
"SplashScreen": {
|
||||
"launchShowDuration": 900,
|
||||
"launchAutoHide": true,
|
||||
"backgroundColor": "#16294F",
|
||||
"showSpinner": false,
|
||||
"iosSpinnerStyle": "large"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+23
-19
@@ -26,7 +26,7 @@
|
||||
/* Safe-area insets. The native app enables edge-to-edge via the @capacitor-community/safe-area plugin,
|
||||
which populates env(safe-area-inset-*) with real values on every device (notch, Dynamic Island, home
|
||||
indicator). On the web these resolve to 0. All layout below pads with var(--sat)/var(--sab). */
|
||||
:root{--sat:env(safe-area-inset-top,0px);--sab:env(safe-area-inset-bottom,0px);--kb:0px;}
|
||||
:root{--sat:env(safe-area-inset-top,0px);--sab:env(safe-area-inset-bottom,0px);}
|
||||
body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--ink);margin:0;display:flex;flex-direction:column;height:100vh;height:100dvh;overflow:hidden;position:fixed;inset:0;width:100%;}
|
||||
|
||||
/* ---- Top bar ---- */
|
||||
@@ -1069,11 +1069,9 @@
|
||||
directly above the keyboard (no nav sandwiched between the text box and the keyboard). The panel
|
||||
then only needs to clear the home indicator. */
|
||||
body.chat-open .rail{display:none;}
|
||||
/* Composer sits on the home indicator normally, and rises to sit exactly on the keyboard when it's
|
||||
open (--kb is set by the Keyboard plugin listener). When the keyboard is up there's no home
|
||||
indicator, so --sab collapses to 0. */
|
||||
body.chat-open .content .panel{bottom:calc(var(--sab) + var(--kb));transition:bottom .22s ease;}
|
||||
body.kb-open{--sab:0px;}
|
||||
/* Composer clears the home indicator; when the keyboard opens, @capacitor/keyboard resize:native
|
||||
shrinks the WebView so this bottom edge ends up right on top of the keyboard — no JS needed. */
|
||||
body.chat-open .content .panel{bottom:var(--sab);}
|
||||
/* 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). */
|
||||
@keyframes bzConvoIn{from{transform:translateX(100%);}to{transform:translateX(0);}}
|
||||
@@ -1112,7 +1110,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<script src="/icons.js?v=6"></script>
|
||||
<script>window.__BUILD='2026-07-17-batch96';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
|
||||
<script>window.__BUILD='2026-07-18-batch97';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,14 +1125,10 @@ 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: the native resize doesn't move our fixed layout, so we drive it ourselves. The Keyboard
|
||||
// plugin reports its height — we expose it as --kb and lift the composer above it (see CSS), then keep
|
||||
// the thread pinned to the newest message.
|
||||
// Keyboard is handled NATIVELY by @capacitor/keyboard (resize:native resizes the WebView so the composer
|
||||
// sits on the keyboard) — no JS height juggling here. We only keep the thread pinned to newest on show.
|
||||
var KB=C.Plugins&&C.Plugins.Keyboard;
|
||||
if(KB && KB.addListener){
|
||||
KB.addListener('keyboardWillShow', function(info){ var h=(info&&info.keyboardHeight)||0; r.style.setProperty('--kb', h+'px'); document.body.classList.add('kb-open'); var b=document.getElementById('msgs'); if(b) requestAnimationFrame(function(){ b.scrollTop=b.scrollHeight; }); });
|
||||
KB.addListener('keyboardWillHide', function(){ r.style.setProperty('--kb','0px'); document.body.classList.remove('kb-open'); });
|
||||
}
|
||||
if(KB && KB.addListener){ KB.addListener('keyboardDidShow', function(){ var b=document.getElementById('msgs'); if(b) b.scrollTop=b.scrollHeight; }); }
|
||||
}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>
|
||||
@@ -1392,6 +1386,7 @@ let ROWS=[]; // sidebar items: {kind:'dm'|'group', id, name, onlin
|
||||
let selected=null; // {kind,id} or null = welcome
|
||||
let convoIsGroup=false; // the open thread is a group (drives per-message sender labels)
|
||||
let THREAD=[];
|
||||
let _convoRevealed=false; // #7: gate the open-time hide/reveal so the cache→network double-render doesn't flash/dance
|
||||
const THREAD_CACHE=new Map(); // key 'kind:id' -> messages[] ; lets a notification click render synchronously (paints immediately)
|
||||
// Pull-to-refresh (mobile): drag down at the top of a scroll area to refresh. Touch-only, so it's
|
||||
// a no-op on desktop. onRefresh is an async function.
|
||||
@@ -2638,12 +2633,17 @@ function renderThread(keepScroll){
|
||||
let html='';
|
||||
for(const m of THREAD){ const dk=dayKey(m.created_at); if(dk!==_lastDay){ html+='<div class="day-sep"><span>'+pEsc(dayLabel(m.created_at))+'</span></div>'; _lastDay=dk; } rendered.add(m.id); html+=bubbleHTML(m); }
|
||||
box.innerHTML=html; twemojify(box);
|
||||
if(!keepScroll){ // #7: keep the thread HIDDEN until its images have loaded (or a short timeout), then
|
||||
// jump to the newest message and reveal — so the user never sees it reflow / "dance" into place.
|
||||
if(!keepScroll){
|
||||
if(_convoRevealed){
|
||||
// #7: a later render during the SAME open (network arriving after the cache render) — just stay at
|
||||
// the newest message, no hide/reveal, so there's no second flash/"dance".
|
||||
box.scrollTop=box.scrollHeight; requestAnimationFrame(()=>{ const b=document.getElementById('msgs'); if(b) b.scrollTop=b.scrollHeight; });
|
||||
} else {
|
||||
// #7: FIRST render of this open — keep the thread hidden until its images have loaded (or a short
|
||||
// timeout), then jump to the newest message and reveal, so it never visibly reflows/"dances".
|
||||
box.style.visibility='hidden'; box.scrollTop=box.scrollHeight;
|
||||
const pending=[...box.querySelectorAll('img')].filter(im=>!im.complete);
|
||||
let revealed=false;
|
||||
const reveal=()=>{ if(revealed) return; revealed=true; const b=document.getElementById('msgs'); if(!b) return; if(!b.querySelector('.new-sep')) b.scrollTop=b.scrollHeight; b.style.visibility='visible'; };
|
||||
const reveal=()=>{ if(_convoRevealed) return; _convoRevealed=true; const b=document.getElementById('msgs'); if(!b) return; if(!b.querySelector('.new-sep')) b.scrollTop=b.scrollHeight; b.style.visibility='visible'; };
|
||||
if(!pending.length){ requestAnimationFrame(reveal); }
|
||||
else {
|
||||
let left=pending.length;
|
||||
@@ -2652,6 +2652,7 @@ function renderThread(keepScroll){
|
||||
setTimeout(reveal, 700); // never wait longer than this for slow images
|
||||
}
|
||||
}
|
||||
}
|
||||
if(_selMode) box.querySelectorAll('.bubble').forEach(b=>{ if(_selIds.has(b.dataset.id)) b.classList.add('selected'); }); // #1: keep selection across re-render
|
||||
}
|
||||
function appendBubble(m){
|
||||
@@ -2679,6 +2680,7 @@ function setDraft(kind,id,val){ try{ if(val&&val.trim()) localStorage.setItem(dr
|
||||
async function openConvo(kind,id){
|
||||
const it=rowFor(kind,id)||{kind,id,name:'Conversation'};
|
||||
const _unreadN=_openUnread; _openUnread=0; // consume the captured unread count (#3)
|
||||
_convoRevealed=false; // #7: this open hasn't revealed yet
|
||||
convoIsGroup=(kind==='group');
|
||||
const el=document.getElementById('chatPanel'); el.classList.remove('center');
|
||||
el.innerHTML=convoShellHTML(it);
|
||||
@@ -2796,7 +2798,9 @@ async function openConvo(kind,id){
|
||||
if(kind==='dm') syncDmRowTick(id); // #1: keep the sidebar tick consistent with the loaded thread
|
||||
// #3: if there were unread messages, drop a "New messages" divider, scroll to the first unread,
|
||||
// and show the "jump to newest" arrow so the user can return to the bottom.
|
||||
if(_unreadN>0 && THREAD.length>=_unreadN){
|
||||
// #7 (mobile): phones open at the LATEST message (no auto-scroll up to the first unread), which is what
|
||||
// users expect from a chat app; the divider/jump-up is a desktop convenience only.
|
||||
if(_unreadN>0 && THREAD.length>=_unreadN && !isMobileUA()){
|
||||
const fu=THREAD[THREAD.length-_unreadN]; const esc=(window.CSS&&CSS.escape)?CSS.escape(fu.id):fu.id;
|
||||
const el=document.querySelector('#msgs .bubble[data-id="'+esc+'"]') || document.querySelector('#msgs [data-id="'+esc+'"]');
|
||||
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'; }
|
||||
|
||||
Reference in New Issue
Block a user