mobile: interactive finger-following swipe-back to go from a conversation to the list (batch137)
Drag the open conversation rightward from the left edge and it follows the finger 1:1 with the list parallaxing in underneath; release past ~35% (or a quick flick) completes the pop, else it snaps back. Fixes the old stuck-pane bug: once a clear horizontal drag is detected we preventDefault (passive:false) to CLAIM the gesture so iOS/scroll can't steal it and fire touchcancel; touchcancel always resolves to a clean state. showWelcome(skipAnim) does the state swap without re-animating. Works in native app, PWA and mobile browser. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+64
-17
@@ -1158,7 +1158,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script src="/icons.js?v=6"></script>
|
<script src="/icons.js?v=6"></script>
|
||||||
<script>window.__BUILD='2026-07-19-batch136';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
|
<script>window.__BUILD='2026-07-19-batch137';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
|
||||||
@@ -4903,11 +4903,12 @@ function switchTab(tab){
|
|||||||
if(tab==='connect' && !loaded.connect){ document.getElementById('connectPanel').innerHTML='<iframe src="/connect?embed=1" allow="camera; microphone; display-capture; clipboard-read; clipboard-write"></iframe>'; loaded.connect=true; }
|
if(tab==='connect' && !loaded.connect){ document.getElementById('connectPanel').innerHTML='<iframe src="/connect?embed=1" allow="camera; microphone; display-capture; clipboard-read; clipboard-write"></iframe>'; loaded.connect=true; }
|
||||||
if(tab==='meeting' && meetState==='idle'){ renderMeetingLobby(); }
|
if(tab==='meeting' && meetState==='idle'){ renderMeetingLobby(); }
|
||||||
}
|
}
|
||||||
function showWelcome(){
|
function showWelcome(skipAnim){
|
||||||
const finish=()=>{ selected=null; document.body.classList.remove('chat-open','chat-closing'); renderChats(searchVal()); renderChatPanel(); updateRailUnread(); };
|
const finish=()=>{ selected=null; document.body.classList.remove('chat-open','chat-closing','chat-dragging'); renderChats(searchVal()); renderChatPanel(); updateRailUnread(); };
|
||||||
// Mobile: slide the outgoing conversation off to the right before swapping back to the chat list, so
|
// Mobile: slide the outgoing conversation off to the right before swapping back to the chat list, so
|
||||||
// "back" has a clear push/pop direction (like Teams/WhatsApp). Desktop just closes instantly.
|
// "back" has a clear push/pop direction (like Teams/WhatsApp). Desktop just closes instantly.
|
||||||
if(document.body.classList.contains('chat-open') && !document.body.classList.contains('chat-closing') && window.matchMedia('(max-width:760px)').matches){
|
// skipAnim=true when the interactive swipe already dragged the pane off-screen — just do the state swap.
|
||||||
|
if(!skipAnim && document.body.classList.contains('chat-open') && !document.body.classList.contains('chat-closing') && window.matchMedia('(max-width:760px)').matches){
|
||||||
const content=document.querySelector('.shell > .content');
|
const content=document.querySelector('.shell > .content');
|
||||||
// Render the list NOW so it's ready underneath before we start sliding the conversation away.
|
// Render the list NOW so it's ready underneath before we start sliding the conversation away.
|
||||||
renderChats(searchVal());
|
renderChats(searchVal());
|
||||||
@@ -4943,22 +4944,68 @@ 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 left-edge
|
// #4: native-style INTERACTIVE swipe-back. Drag the open conversation rightward from the left edge and it
|
||||||
// SWIPE-back: a drag that begins at the very left edge and moves clearly rightward closes the topmost
|
// FOLLOWS YOUR FINGER 1:1, with the chat list parallaxing in underneath; release past ~35% of the width (or
|
||||||
// layer (popup → in-conversation search → open chat) via bzcBack() — which runs showWelcome()'s parallax
|
// a quick flick) completes the pop, otherwise it snaps back. The old version's bug — the pane getting stuck
|
||||||
// slide. This is RELEASE-triggered on purpose: an interactive finger-follow drag conflicts with iOS's own
|
// — happened because passive listeners let iOS/scroll steal the gesture and fire touchcancel. Fix: once a
|
||||||
// edge gesture (it fires touchcancel), which left the UI stuck; release-based never leaves that state.
|
// clear horizontal drag is detected we preventDefault (passive:false) to CLAIM the gesture, and touchcancel
|
||||||
|
// always resolves to a clean state via end()/reset(). Arms only in the left 32px so it never clashes with
|
||||||
|
// the bubble swipe-to-reply (which starts >32px). Works in the native app, PWA and mobile browser alike.
|
||||||
(function(){
|
(function(){
|
||||||
let sx=0, sy=0, edge=false;
|
const EDGE=32;
|
||||||
|
let sx=0, sy=0, w=0, armed=false, dragging=false, content=null, chatcol=null, t0=0;
|
||||||
|
const isMobile=()=>window.matchMedia('(max-width:760px)').matches;
|
||||||
|
function canSwipe(){
|
||||||
|
if(!isMobile() || selected==null) return false;
|
||||||
|
if(!document.body.classList.contains('chat-open') || document.body.classList.contains('chat-closing')) return false;
|
||||||
|
if(document.querySelector('.modal-ov') || document.getElementById('lightbox')) return false;
|
||||||
|
const sh=document.getElementById('convoSearchHead'); if(sh && sh.style.display==='flex') return false; // let search close via its own X / the back button
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
function reset(){ if(content){ content.style.transition=''; content.style.transform=''; } if(chatcol){ chatcol.style.transition=''; chatcol.style.transform=''; } document.body.classList.remove('chat-dragging'); armed=false; dragging=false; content=null; chatcol=null; }
|
||||||
window.addEventListener('touchstart',(e)=>{
|
window.addEventListener('touchstart',(e)=>{
|
||||||
if(e.touches.length!==1){ edge=false; return; }
|
if(dragging) return;
|
||||||
const t=e.touches[0]; edge = t.clientX<=24; sx=t.clientX; sy=t.clientY; // only arm when starting at the left edge
|
if(e.touches.length!==1 || !canSwipe()){ armed=false; return; }
|
||||||
}, {passive:true});
|
const t=e.touches[0];
|
||||||
window.addEventListener('touchend',(e)=>{
|
if(t.clientX>EDGE){ armed=false; return; }
|
||||||
if(!edge) return; edge=false;
|
armed=true; dragging=false; sx=t.clientX; sy=t.clientY; w=window.innerWidth||360; t0=Date.now();
|
||||||
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});
|
}, {passive:true});
|
||||||
|
window.addEventListener('touchmove',(e)=>{
|
||||||
|
if(!armed || e.touches.length!==1) return;
|
||||||
|
const t=e.touches[0], dx=t.clientX-sx, dy=t.clientY-sy;
|
||||||
|
if(!dragging){
|
||||||
|
if(Math.abs(dy)>Math.abs(dx) && Math.abs(dy)>8){ armed=false; return; } // vertical scroll → release
|
||||||
|
if(dx>10 && dx>=Math.abs(dy)){ // confirmed rightward → claim it
|
||||||
|
dragging=true;
|
||||||
|
content=document.querySelector('.shell > .content');
|
||||||
|
chatcol=document.getElementById('chatcol');
|
||||||
|
renderChats(searchVal()); // make the list under the pane current
|
||||||
|
document.body.classList.add('chat-dragging');
|
||||||
|
if(content) content.style.transition='none';
|
||||||
|
if(chatcol) chatcol.style.transition='none';
|
||||||
|
} else return;
|
||||||
|
}
|
||||||
|
e.preventDefault(); // needs passive:false — claims the gesture
|
||||||
|
const x=Math.max(0, dx);
|
||||||
|
if(content) content.style.transform='translate3d('+x+'px,0,0)';
|
||||||
|
if(chatcol) chatcol.style.transform='translate3d('+(-22*(1-Math.min(1,x/w)))+'%,0,0)';
|
||||||
|
}, {passive:false});
|
||||||
|
function end(e){
|
||||||
|
if(!dragging){ armed=false; return; }
|
||||||
|
const t=(e.changedTouches&&e.changedTouches[0]);
|
||||||
|
const dx=t?Math.max(0,t.clientX-sx):0, dt=Math.max(1,Date.now()-t0), v=dx/dt;
|
||||||
|
if(dx>w*0.35 || (v>0.5 && dx>40)){ // complete the pop
|
||||||
|
if(content){ content.style.transition='transform .17s cubic-bezier(.4,0,.7,.4)'; content.style.transform='translate3d(100%,0,0)'; }
|
||||||
|
if(chatcol){ chatcol.style.transition='transform .17s cubic-bezier(.4,0,.7,.4)'; chatcol.style.transform='translate3d(0,0,0)'; }
|
||||||
|
setTimeout(()=>{ document.body.classList.remove('chat-dragging'); showWelcome(true); if(content){ content.style.transition=''; content.style.transform=''; } if(chatcol){ chatcol.style.transition=''; chatcol.style.transform=''; } armed=false; dragging=false; content=null; chatcol=null; }, 175);
|
||||||
|
} else { // snap back — stay in the chat
|
||||||
|
if(content){ content.style.transition='transform .17s'; content.style.transform='translate3d(0,0,0)'; }
|
||||||
|
if(chatcol){ chatcol.style.transition='transform .17s'; chatcol.style.transform='translate3d(-22%,0,0)'; }
|
||||||
|
setTimeout(reset, 175);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('touchend', end, {passive:true});
|
||||||
|
window.addEventListener('touchcancel',(e)=>{ if(dragging) end(e); else armed=false; }, {passive:true}); // never leave the pane stuck
|
||||||
})();
|
})();
|
||||||
// #9: swipe a message bubble to the right to reply to it (mobile), like WhatsApp/Teams. The bubble
|
// #9: swipe a message bubble to the right to reply to it (mobile), like WhatsApp/Teams. The bubble
|
||||||
// follows the finger a little; releasing past the threshold opens the reply composer for that message.
|
// follows the finger a little; releasing past the threshold opens the reply composer for that message.
|
||||||
|
|||||||
Reference in New Issue
Block a user