fix(mobile): parallax slide-back, swipe images left/right, glue thread while images load
- #4: on back, reveal the chat list UNDERNEATH with a parallax slide-in while the conversation slides off to the right (slide the whole .content, list forced visible beneath) — matches the Teams push/pop. - #8: swipe left/right in the image viewer flips to the prev/next image (not just swipe-down to close). - #7: re-pin to the bottom as each image finishes loading (when near bottom), so late images don't reflow/'dance' the thread. Build marker -> 2026-07-17-batch94. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+20
-9
@@ -1073,11 +1073,13 @@
|
|||||||
/* Native-style push/pop: the conversation slides in from the right when opened and slides back out
|
/* 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). */
|
when you go back, so navigation has a clear direction (like Teams/WhatsApp). */
|
||||||
@keyframes bzConvoIn{from{transform:translateX(100%);}to{transform:translateX(0);}}
|
@keyframes bzConvoIn{from{transform:translateX(100%);}to{transform:translateX(0);}}
|
||||||
|
@keyframes bzListIn{from{transform:translateX(-24%);}to{transform:translateX(0);}}
|
||||||
body.chat-open:not(.chat-closing) .content .panel{animation:bzConvoIn .22s cubic-bezier(.2,.7,.3,1);}
|
body.chat-open:not(.chat-closing) .content .panel{animation:bzConvoIn .22s cubic-bezier(.2,.7,.3,1);}
|
||||||
body.chat-closing .content{display:block!important;}
|
/* Close (native push/pop, like Teams): reveal the chat LIST underneath with a parallax slide-in while
|
||||||
/* Close: the outgoing conversation is slid out via an inline transform in showWelcome() — more
|
the conversation slides off to the right. The conversation (.content) is slid by an inline transform
|
||||||
reliable across WebKit than swapping @keyframes on the same element mid-flight. */
|
in showWelcome(); the list (.chatcol) is force-shown beneath it for the duration of the animation. */
|
||||||
body.chat-closing .content .panel{will-change:transform;}
|
body.chat-closing .content{display:block!important;z-index:2;box-shadow:-10px 0 26px rgba(15,23,42,.16);will-change:transform;}
|
||||||
|
body.chat-closing .chatcol{display:flex!important;position:absolute;inset:0;z-index:0;animation:bzListIn .2s cubic-bezier(.4,0,.7,.4);}
|
||||||
/* 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;}
|
||||||
@@ -1104,7 +1106,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script src="/icons.js?v=6"></script>
|
<script src="/icons.js?v=6"></script>
|
||||||
<script>window.__BUILD='2026-07-17-batch93';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
|
<script>window.__BUILD='2026-07-17-batch94';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
|
||||||
@@ -2567,7 +2569,11 @@ function openLightbox(src){
|
|||||||
let dsy=0, dsx=0, dDrag=false;
|
let dsy=0, dsx=0, dDrag=false;
|
||||||
img.addEventListener('touchstart',(e)=>{ if(z<=1 && e.touches.length===1){ dDrag=true; dsy=e.touches[0].clientY; dsx=e.touches[0].clientX; } },{passive:true});
|
img.addEventListener('touchstart',(e)=>{ if(z<=1 && e.touches.length===1){ dDrag=true; dsy=e.touches[0].clientY; dsx=e.touches[0].clientX; } },{passive:true});
|
||||||
img.addEventListener('touchmove',(e)=>{ if(!dDrag||z>1||e.touches.length!==1) return; const dy=e.touches[0].clientY-dsy, dx=e.touches[0].clientX-dsx; if(dy>0 && dy>Math.abs(dx)){ img.style.transition='none'; img.style.transform='translateY('+dy+'px)'; ov.style.background='rgba(0,0,0,'+Math.max(.15,0.92-dy/500)+')'; } },{passive:true});
|
img.addEventListener('touchmove',(e)=>{ if(!dDrag||z>1||e.touches.length!==1) return; const dy=e.touches[0].clientY-dsy, dx=e.touches[0].clientX-dsx; if(dy>0 && dy>Math.abs(dx)){ img.style.transition='none'; img.style.transform='translateY('+dy+'px)'; ov.style.background='rgba(0,0,0,'+Math.max(.15,0.92-dy/500)+')'; } },{passive:true});
|
||||||
img.addEventListener('touchend',(e)=>{ if(!dDrag) return; dDrag=false; const t=e.changedTouches[0]; const dy=t?(t.clientY-dsy):0; if(dy>100 && z<=1){ close(); } else { img.style.transition='transform .18s'; img.style.transform=''; ov.style.background=''; setTimeout(()=>{ if(img) img.style.transition=''; },200); } },{passive:true});
|
img.addEventListener('touchend',(e)=>{ if(!dDrag) return; dDrag=false; const t=e.changedTouches[0]; const dy=t?(t.clientY-dsy):0, dx=t?(t.clientX-dsx):0;
|
||||||
|
if(z<=1 && dy>100 && dy>Math.abs(dx)){ close(); return; } // #8: swipe down = dismiss
|
||||||
|
if(z<=1 && multi && Math.abs(dx)>50 && Math.abs(dx)>Math.abs(dy)){ show(dx<0?idx+1:idx-1); } // #8: swipe left/right = prev/next image
|
||||||
|
img.style.transition='transform .18s'; img.style.transform=''; ov.style.background=''; setTimeout(()=>{ if(img) img.style.transition=''; },200);
|
||||||
|
},{passive:true});
|
||||||
const onKey=(e)=>{
|
const onKey=(e)=>{
|
||||||
if(e.key==='Escape'){ e.preventDefault(); if(z>1){ resetZoom(); return; } close(); }
|
if(e.key==='Escape'){ e.preventDefault(); if(z>1){ resetZoom(); return; } close(); }
|
||||||
else if(e.key==='ArrowLeft'){ e.preventDefault(); show(idx-1); }
|
else if(e.key==='ArrowLeft'){ e.preventDefault(); show(idx-1); }
|
||||||
@@ -2621,6 +2627,9 @@ function renderThread(keepScroll){
|
|||||||
const pin=()=>{ const b=document.getElementById('msgs'); if(b && !b.querySelector('.new-sep')) b.scrollTop=b.scrollHeight; };
|
const pin=()=>{ const b=document.getElementById('msgs'); if(b && !b.querySelector('.new-sep')) b.scrollTop=b.scrollHeight; };
|
||||||
requestAnimationFrame(()=>{ pin(); box.style.visibility='visible'; });
|
requestAnimationFrame(()=>{ pin(); box.style.visibility='visible'; });
|
||||||
setTimeout(pin,150); setTimeout(pin,400);
|
setTimeout(pin,150); setTimeout(pin,400);
|
||||||
|
// As each image finishes loading, re-glue to the bottom (only while the user is still near it) so
|
||||||
|
// late-loading images can't visibly reflow / "dance" the thread.
|
||||||
|
box.querySelectorAll('img').forEach(im=>{ if(!im.complete) im.addEventListener('load', ()=>{ const b=document.getElementById('msgs'); if(b && !b.querySelector('.new-sep') && (b.scrollHeight-b.scrollTop-b.clientHeight)<400) b.scrollTop=b.scrollHeight; }, {once:true}); });
|
||||||
}
|
}
|
||||||
if(_selMode) box.querySelectorAll('.bubble').forEach(b=>{ if(_selIds.has(b.dataset.id)) b.classList.add('selected'); }); // #1: keep selection across re-render
|
if(_selMode) box.querySelectorAll('.bubble').forEach(b=>{ if(_selIds.has(b.dataset.id)) b.classList.add('selected'); }); // #1: keep selection across re-render
|
||||||
}
|
}
|
||||||
@@ -4631,10 +4640,12 @@ function showWelcome(){
|
|||||||
// 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){
|
if(document.body.classList.contains('chat-open') && !document.body.classList.contains('chat-closing') && window.matchMedia('(max-width:760px)').matches){
|
||||||
const panel=document.querySelector('.content .panel[data-panel="chat"]');
|
const content=document.querySelector('.shell > .content');
|
||||||
|
// Render the list NOW so it's ready underneath before we start sliding the conversation away.
|
||||||
|
renderChats(searchVal());
|
||||||
document.body.classList.add('chat-closing');
|
document.body.classList.add('chat-closing');
|
||||||
if(panel){ panel.style.transition='transform .2s cubic-bezier(.4,0,.7,.4)'; requestAnimationFrame(()=>{ panel.style.transform='translateX(100%)'; }); }
|
if(content){ content.style.transition='transform .21s cubic-bezier(.4,0,.7,.4)'; requestAnimationFrame(()=>{ content.style.transform='translateX(100%)'; }); }
|
||||||
setTimeout(()=>{ if(panel){ panel.style.transition=''; panel.style.transform=''; } finish(); }, 210);
|
setTimeout(()=>{ if(content){ content.style.transition=''; content.style.transform=''; } finish(); }, 215);
|
||||||
} else finish();
|
} else finish();
|
||||||
}
|
}
|
||||||
railBtns.forEach(btn=>{ btn.onclick=()=>{
|
railBtns.forEach(btn=>{ btn.onclick=()=>{
|
||||||
|
|||||||
Reference in New Issue
Block a user