fix(ios/mobile): safe-area insets, no keyboard auto-open, no h-scroll, swipe-back, hide desktop-download

Reported on the iOS TestFlight build:
- #1/#5 notch/home-indicator: apply top safe-area inset per surface (chat list column +
  content panels) instead of on the whole shell, so headers sit below the Dynamic Island
  and nothing is cut off top/bottom.
- #2: opening a chat no longer auto-focuses the composer (was popping the phone keyboard
  and shoving the layout up); tapping the box still focuses normally.
- #3: overflow-x:hidden + max-width:100vw guards so pages can't pan left/right.
- #4: left-edge swipe-right now triggers the existing bzcBack() (close popup/search/open
  chat) — a native-feeling back gesture with no native code.
- #6: hide the 'Download app' (Windows) button when running inside the Capacitor app.
Build marker -> 2026-07-17-batch90.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 16:44:12 +05:30
parent 2e4e0210b0
commit 95e13a707e
2 changed files with 32 additions and 10 deletions
+27 -6
View File
@@ -1020,7 +1020,13 @@
relocated into the chat-list header (see placeHdrRight). The shell is pushed below the relocated into the chat-list header (see placeHdrRight). The shell is pushed below the
device status bar so no view overlaps it. */ device status bar so no view overlaps it. */
header{display:none;} header{display:none;}
.shell{padding-top:env(safe-area-inset-top,0px);} /* #3: kill any horizontal drift on phones (long content, 100vw children, safe-area math) — the page
should never pan left/right. */
html,body{overflow-x:hidden;max-width:100vw;}
.shell,.content,.chatcol,.panel,.convo,.convo-msgs,.chatlist{overflow-x:hidden;max-width:100%;}
/* #1/#5: the top safe-area inset (notch / Dynamic Island) is applied PER SURFACE — the chat-list
column and each content panel — rather than on the whole shell, so nothing hides under the island
and there's no double-padding. Bottom inset is handled by the fixed nav + panel bottom below. */
.side-title{gap:.5rem;} .side-title{gap:.5rem;}
.side-title h2{flex:1;} .side-title h2{flex:1;}
.side-title #hdrRight{display:flex;align-items:center;gap:.4rem;flex:0 0 auto;} .side-title #hdrRight{display:flex;align-items:center;gap:.4rem;flex:0 0 auto;}
@@ -1046,12 +1052,12 @@
/* Meeting room code: always fully visible, full-width, easy to read */ /* Meeting room code: always fully visible, full-width, easy to read */
.meet-bar .code{flex:1 1 100%;font-size:.9rem;white-space:normal;word-break:break-word;text-align:center;background:var(--blue-soft);border-radius:8px;padding:.35rem .5rem;} .meet-bar .code{flex:1 1 100%;font-size:.9rem;white-space:normal;word-break:break-word;text-align:center;background:var(--blue-soft);border-radius:8px;padding:.35rem .5rem;}
/* Chat: one pane at a time (list, then the open conversation) */ /* Chat: one pane at a time (list, then the open conversation) */
.chatcol{width:100%;flex:1 1 100%;padding-bottom:calc(60px + env(safe-area-inset-bottom,0px));} .chatcol{width:100%;flex:1 1 100%;padding-top:env(safe-area-inset-top,0px);padding-bottom:calc(60px + env(safe-area-inset-bottom,0px));}
body.chat-open .chatcol{display:none;} body.chat-open .chatcol{display:none;}
.content{display:none;} .content{display:none;}
body.chat-open .content, .shell:not(.is-chat) .content{display:block;} body.chat-open .content, .shell:not(.is-chat) .content{display:block;}
.shell:not(.is-chat) .chatcol{display:none;} .shell:not(.is-chat) .chatcol{display:none;}
.content .panel{bottom:calc(60px + env(safe-area-inset-bottom,0px));} /* clear the bottom nav + home indicator */ .content .panel{top:env(safe-area-inset-top,0px);bottom:calc(60px + env(safe-area-inset-bottom,0px));} /* below the notch; clear the bottom nav + home indicator */
/* Bigger touch targets */ /* Bigger touch targets */
.chat-row{padding:.7rem .85rem;} .chat-row{padding:.7rem .85rem;}
.convo-head{padding:.7rem .85rem;} .convo-head{padding:.7rem .85rem;}
@@ -1078,7 +1084,7 @@
</head> </head>
<body> <body>
<script src="/icons.js?v=6"></script> <script src="/icons.js?v=6"></script>
<script>window.__BUILD='2026-07-15-batch89';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD); <script>window.__BUILD='2026-07-17-batch90';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
@@ -2713,7 +2719,7 @@ async function openConvo(kind,id){
// are still safely on the server → the "messages disappear after reopen" bug). Keep the cached // are still safely on the server → the "messages disappear after reopen" bug). Keep the cached
// render; only show empty if we truly have nothing cached to fall back to. // render; only show empty if we truly have nothing cached to fall back to.
if(!THREAD_CACHE.has(ckey)){ THREAD=[]; renderThread(); } if(!THREAD_CACHE.has(ckey)){ THREAD=[]; renderThread(); }
const inp0=document.getElementById('msgInput'); if(inp0) inp0.focus(); const inp0=document.getElementById('msgInput'); if(inp0 && !isMobileUA()) inp0.focus(); // #2: don't auto-open the phone keyboard on chat open
return; return;
} }
THREAD=msgs; THREAD=msgs;
@@ -2728,7 +2734,7 @@ async function openConvo(kind,id){
const el=document.querySelector('#msgs .bubble[data-id="'+esc+'"]') || document.querySelector('#msgs [data-id="'+esc+'"]'); 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'; } 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) inp.focus(); 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)
} }
// ---------- @mentions (group chat) ---------- // ---------- @mentions (group chat) ----------
let mentionItems=[], mentionIdx=0, mentionStart=-1; let mentionItems=[], mentionIdx=0, mentionStart=-1;
@@ -4611,6 +4617,21 @@ function bzcBack(){
} }
try{ history.pushState(null,'',location.href); }catch(_){} try{ history.pushState(null,'',location.href); }catch(_){}
window.addEventListener('popstate', ()=>{ if(bzcBack()){ try{ history.pushState(null,'',location.href); }catch(_){} } }); window.addEventListener('popstate', ()=>{ if(bzcBack()){ try{ history.pushState(null,'',location.href); }catch(_){} } });
// #4: iOS/Android have no on-screen back button in a full-screen webview, so give the app a native-feeling
// left-edge SWIPE-back: a drag that begins at the very left edge and moves clearly rightward closes the
// topmost layer (popup → in-conversation search → open chat) via the same bzcBack() used for the OS back.
(function(){
let sx=0, sy=0, edge=false;
window.addEventListener('touchstart',(e)=>{
if(e.touches.length!==1){ edge=false; return; }
const t=e.touches[0]; edge = t.clientX<=24; sx=t.clientX; sy=t.clientY; // only arm when starting at the left edge
}, {passive:true});
window.addEventListener('touchend',(e)=>{
if(!edge) return; edge=false;
const t=e.changedTouches&&e.changedTouches[0]; if(!t) return;
if((t.clientX-sx)>60 && Math.abs(t.clientY-sy)<50) bzcBack(); // rightward, mostly-horizontal swipe = back
}, {passive:true});
})();
// Hamburger: on mobile it slides the rail drawer in/out; on desktop it collapses the rail. // Hamburger: on mobile it slides the rail drawer in/out; on desktop it collapses the rail.
function toggleNav(){ if(window.innerWidth<=760) document.body.classList.toggle('rail-open'); else document.body.classList.toggle('rail-hidden'); } function toggleNav(){ if(window.innerWidth<=760) document.body.classList.toggle('rail-open'); else document.body.classList.toggle('rail-hidden'); }
const _navT=document.getElementById('navToggle'); if(_navT) _navT.onclick=toggleNav; const _navT=document.getElementById('navToggle'); if(_navT) _navT.onclick=toggleNav;
+5 -4
View File
@@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Biz Connect</title> <title>Biz Connect</title>
<meta name="theme-color" content="#1F3B73"> <meta name="theme-color" content="#1F3B73">
<link rel="icon" href="/favicon.ico?v=3" sizes="any"> <link rel="icon" href="/favicon.ico?v=3" sizes="any">
@@ -14,7 +14,7 @@
:root{ --brand:#FFC708; --brand-d:#E0AC00; --blue:#1F3B73; --blue-d:#16294f; --blue-soft:#EAF0FB; --ink:#1f2430; --muted:#6b7280; --bg:#f6f8fb; --line:#e6e9ef; } :root{ --brand:#FFC708; --brand-d:#E0AC00; --blue:#1F3B73; --blue-d:#16294f; --blue-soft:#EAF0FB; --ink:#1f2430; --muted:#6b7280; --bg:#f6f8fb; --line:#e6e9ef; }
*{box-sizing:border-box;} *{box-sizing:border-box;}
body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--ink);margin:0;min-height:100vh;display:flex;flex-direction:column;} body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--ink);margin:0;min-height:100vh;display:flex;flex-direction:column;}
header{background:var(--blue);padding:.85rem 1.5rem;display:flex;justify-content:space-between;align-items:center;} header{background:var(--blue);padding:calc(.85rem + env(safe-area-inset-top,0px)) 1.5rem .85rem;display:flex;justify-content:space-between;align-items:center;}
.brandrow{display:flex;align-items:center;gap:.7rem;} .brandrow{display:flex;align-items:center;gap:.7rem;}
.hdr-right{display:flex;align-items:center;gap:.9rem;} .hdr-right{display:flex;align-items:center;gap:.9rem;}
.dl-btn{display:inline-flex;align-items:center;gap:.4rem;background:#fff;color:var(--blue);padding:.45rem .85rem;border-radius:9px;font-size:.85rem;font-weight:700;text-decoration:none;white-space:nowrap;box-shadow:0 2px 8px rgba(0,0,0,.12);} .dl-btn{display:inline-flex;align-items:center;gap:.4rem;background:#fff;color:var(--blue);padding:.45rem .85rem;border-radius:9px;font-size:.85rem;font-weight:700;text-decoration:none;white-space:nowrap;box-shadow:0 2px 8px rgba(0,0,0,.12);}
@@ -88,8 +88,9 @@ function profileHTML(name){return '<div class="profile"><button class="pbtn" id=
function wireProfile(){const btn=document.getElementById('pbtn'),menu=document.getElementById('pmenu');if(!btn)return;btn.onclick=(e)=>{e.stopPropagation();menu.classList.toggle('open');};document.addEventListener('click',()=>menu.classList.remove('open'));const lo=document.getElementById('plogout');if(lo)lo.onclick=async()=>{try{await fetch('/api/logout',{method:'POST'});}catch(_){}location.href='/';};} function wireProfile(){const btn=document.getElementById('pbtn'),menu=document.getElementById('pmenu');if(!btn)return;btn.onclick=(e)=>{e.stopPropagation();menu.classList.toggle('open');};document.addEventListener('click',()=>menu.classList.remove('open'));const lo=document.getElementById('plogout');if(lo)lo.onclick=async()=>{try{await fetch('/api/logout',{method:'POST'});}catch(_){}location.href='/';};}
function makeBrandClickable(){document.querySelectorAll('.brandrow,.wordmark').forEach(el=>{el.style.cursor='pointer';el.addEventListener('click',()=>{location.href='/';});});} function makeBrandClickable(){document.querySelectorAll('.brandrow,.wordmark').forEach(el=>{el.style.cursor='pointer';el.addEventListener('click',()=>{location.href='/';});});}
makeBrandClickable(); makeBrandClickable();
// No "download" link when we're already inside the desktop app. // No "download the desktop app" link when we're already inside a native shell — the Electron desktop
if(window.__NATIVE__){var _dl=document.getElementById('dlWin');if(_dl)_dl.style.display='none';} // app (window.__NATIVE__) OR the iOS/Android Capacitor app (window.Capacitor.isNativePlatform()).
if(window.__NATIVE__ || (window.Capacitor && window.Capacitor.isNativePlatform && window.Capacitor.isNativePlatform())){var _dl=document.getElementById('dlWin');if(_dl)_dl.style.display='none';}
// Already signed in -> skip this landing entirely and go straight to the app (no redundant // Already signed in -> skip this landing entirely and go straight to the app (no redundant
// "Open Biz Connect" page). This landing only shows when logged out. // "Open Biz Connect" page). This landing only shows when logged out.
(async function(){try{const r=await fetch('/api/me');if(r.ok){ location.replace('/home'); return; }}catch(_){}})(); (async function(){try{const r=await fetch('/api/me');if(r.ok){ location.replace('/home'); return; }}catch(_){}})();