diff --git a/mobile/capacitor.config.json b/mobile/capacitor.config.json index 7453b1b..cb11352 100644 --- a/mobile/capacitor.config.json +++ b/mobile/capacitor.config.json @@ -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" } } } diff --git a/server/public/home.html b/server/public/home.html index 70b7946..6336e29 100644 --- a/server/public/home.html +++ b/server/public/home.html @@ -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 @@ - // juggling here — we only tag 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(_){} })();
Biz Connect
Loading…
@@ -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,18 +2633,24 @@ function renderThread(keepScroll){ let html=''; for(const m of THREAD){ const dk=dayKey(m.created_at); if(dk!==_lastDay){ html+='
'+pEsc(dayLabel(m.created_at))+'
'; _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. - 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'; }; - if(!pending.length){ requestAnimationFrame(reveal); } - else { - let left=pending.length; - const one=()=>{ if(--left<=0) reveal(); }; - pending.forEach(im=>{ const h=()=>{ im.removeEventListener('load',h); im.removeEventListener('error',h); one(); }; im.addEventListener('load',h); im.addEventListener('error',h); }); - setTimeout(reveal, 700); // never wait longer than this for slow images + 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); + 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; + const one=()=>{ if(--left<=0) reveal(); }; + pending.forEach(im=>{ const h=()=>{ im.removeEventListener('load',h); im.removeEventListener('error',h); one(); }; im.addEventListener('load',h); im.addEventListener('error',h); }); + 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 @@ -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='New messages'; el.parentNode.insertBefore(sep, el); el.scrollIntoView({block:'start'}); el.parentNode.scrollTop-=40; const jl=document.getElementById('jumpLatest'); if(jl) jl.style.display='grid'; }