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:
+27
-6
@@ -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;
|
||||||
|
|||||||
@@ -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(_){}})();
|
||||||
|
|||||||
Reference in New Issue
Block a user