mobile: chat open = instant + quick opacity fade (no slide) (batch128)
User chose the fade over chasing the WebView slide-teardown reflow. Open now: the cache- rendered conversation is placed instantly and fades opacity 0->1 over 140ms. No transform, no off-screen pane, no list-column display toggle -> nothing to reflow at the end, so the ~40ms end-of-slide settle cannot occur. Same afterOpenSlide queue flushes network reconcile/ pins/appends at fade-end. Desktop unaffected (fade is mobile-only via __freshOpen). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+21
-15
@@ -1110,10 +1110,11 @@
|
|||||||
its own promoted GPU layer while the message DOM renders + scrolls to bottom. Phase 2 (.chat-anim,
|
its own promoted GPU layer while the message DOM renders + scrolls to bottom. Phase 2 (.chat-anim,
|
||||||
added by JS on a double-rAF AFTER the render is laid out & painted): the compositor transitions
|
added by JS on a double-rAF AFTER the render is laid out & painted): the compositor transitions
|
||||||
transform 100%→0 with the main thread idle → a true 60fps slide, not a jump. Dropped at transitionend. */
|
transform 100%→0 with the main thread idle → a true 60fps slide, not a jump. Dropped at transitionend. */
|
||||||
body.chat-opening .content{display:block!important;transform:translate3d(100%,0,0);} /* ONLY the start transform — display/z-index/box-shadow/will-change all come from the resting body.chat-open rule above, so removing chat-opening/chat-anim at transitionend changes NOTHING (transform 0→none is a no-op, the layer stays promoted). Zero teardown = zero end-of-slide hitch. */
|
/* OPEN = instant position + a quick opacity fade. No transform, no parallax, no list-column display
|
||||||
body.chat-opening.chat-anim .content{transform:translate3d(0,0,0);transition:transform .3s cubic-bezier(.32,.72,0,1);}
|
toggle → there is literally nothing to reflow when it finishes, so the old ~40ms end-of-slide settle
|
||||||
body.chat-opening .chatcol{display:flex!important;position:absolute;inset:0;z-index:0;transform:translate3d(0,0,0);will-change:transform;backface-visibility:hidden;}
|
is gone by construction. The list (chatcol) stays display:none throughout; content fades in over the bg. */
|
||||||
body.chat-opening.chat-anim .chatcol{transform:translate3d(-22%,0,0);transition:transform .3s cubic-bezier(.32,.72,0,1);} /* translate-only parallax — animating filter:brightness re-rasters the list every frame AND again on teardown; the dim isn't worth the jank. */
|
body.chat-opening .content{display:block!important;opacity:0;}
|
||||||
|
body.chat-opening .content.chat-fadein{opacity:1;transition:opacity .14s ease-out;}
|
||||||
/* CLOSE / drag-back: reveal the list underneath (parallax in) while the conversation slides off right
|
/* CLOSE / drag-back: reveal the list underneath (parallax in) while the conversation slides off right
|
||||||
(.content moved by an inline transform in showWelcome()/the edge-drag gesture). */
|
(.content moved by an inline transform in showWelcome()/the edge-drag gesture). */
|
||||||
body.chat-closing .content, body.chat-dragging .content{display:block!important;z-index:2;box-shadow:-12px 0 30px rgba(15,23,42,.18);will-change:transform;}
|
body.chat-closing .content, body.chat-dragging .content{display:block!important;z-index:2;box-shadow:-12px 0 30px rgba(15,23,42,.18);will-change:transform;}
|
||||||
@@ -1145,7 +1146,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-batch127';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
|
<script>window.__BUILD='2026-07-19-batch128';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
|
||||||
@@ -2817,26 +2818,31 @@ let _slideQueue=[], _slideEndHandler=null;
|
|||||||
function afterOpenSlide(fn){ if(!document.body.classList.contains('chat-opening')){ fn(); return; } _slideQueue.push(fn); }
|
function afterOpenSlide(fn){ if(!document.body.classList.contains('chat-opening')){ fn(); return; } _slideQueue.push(fn); }
|
||||||
function flushSlideQueue(){ const q=_slideQueue; _slideQueue=[]; for(const fn of q){ try{ fn(); }catch(_){} } }
|
function flushSlideQueue(){ const q=_slideQueue; _slideQueue=[]; for(const fn of q){ try{ fn(); }catch(_){} } }
|
||||||
function armOpenSlide(){
|
function armOpenSlide(){
|
||||||
|
// OPEN = instant + a quick opacity fade (user-chosen). The cache-rendered conversation is already in place;
|
||||||
|
// we just fade it from 0→1. No transform, no off-screen pane, no list-column display toggle → nothing to
|
||||||
|
// reflow when it ends, so the old end-of-slide settle cannot occur. Queued network reconcile / pins / live
|
||||||
|
// appends still flush at fade-end via the same afterOpenSlide mechanism. Token guards rapid chat re-taps.
|
||||||
const b=document.body, c=document.querySelector('.shell > .content');
|
const b=document.body, c=document.querySelector('.shell > .content');
|
||||||
const token=(window.__slideTok=(window.__slideTok||0)+1);
|
const token=(window.__slideTok=(window.__slideTok||0)+1);
|
||||||
if(c && _slideEndHandler){ c.removeEventListener('transitionend', _slideEndHandler); _slideEndHandler=null; }
|
if(c && _slideEndHandler){ c.removeEventListener('transitionend', _slideEndHandler); _slideEndHandler=null; }
|
||||||
clearTimeout(window.__chatOpenT);
|
clearTimeout(window.__chatOpenT);
|
||||||
b.classList.remove('chat-anim');
|
if(c) c.classList.remove('chat-fadein');
|
||||||
b.classList.add('chat-opening'); // pane -> translate3d(100%), off-screen, own GPU layer
|
b.classList.add('chat-opening'); // content -> opacity:0 (still laid out, just invisible)
|
||||||
const done=()=>{ if(window.__slideTok!==token) return;
|
const done=()=>{ if(window.__slideTok!==token) return;
|
||||||
if(c && _slideEndHandler){ c.removeEventListener('transitionend', _slideEndHandler); _slideEndHandler=null; }
|
if(c && _slideEndHandler){ c.removeEventListener('transitionend', _slideEndHandler); _slideEndHandler=null; }
|
||||||
clearTimeout(window.__chatOpenT);
|
clearTimeout(window.__chatOpenT);
|
||||||
b.classList.remove('chat-opening','chat-anim'); // teardown is now a no-op: the layer stays promoted via body.chat-open, so there's no demote raster to move off the motion path
|
b.classList.remove('chat-opening'); // opacity returns to the resting 1 — no transform/list toggle to reflow
|
||||||
flushSlideQueue(); // chat-opening now OFF → queued appendBubble()s run inline instead of re-deferring into a dead queue
|
if(c) c.classList.remove('chat-fadein');
|
||||||
|
flushSlideQueue(); // network reconcile / pins / queued appends run now
|
||||||
};
|
};
|
||||||
_slideEndHandler=function(e){ if(e.target===c && e.propertyName==='transform') done(); };
|
_slideEndHandler=function(e){ if(e.target===c && e.propertyName==='opacity') done(); };
|
||||||
if(c) c.addEventListener('transitionend', _slideEndHandler);
|
if(c) c.addEventListener('transitionend', _slideEndHandler);
|
||||||
requestAnimationFrame(()=>requestAnimationFrame(()=>{
|
requestAnimationFrame(()=>{
|
||||||
if(window.__slideTok!==token) return;
|
if(window.__slideTok!==token) return;
|
||||||
if(window.__perfProbe) window.__perfProbe('slide'); // TEMP: measure frame timing over the slide
|
if(window.__perfProbe) window.__perfProbe('slide'); // TEMP: measure frame timing over the open
|
||||||
b.classList.add('chat-anim'); // compositor now runs transform 100%->0
|
if(c) c.classList.add('chat-fadein'); // opacity 0 -> 1 over 140ms (compositor-only)
|
||||||
window.__chatOpenT=setTimeout(done, 360); // fallback if transitionend is missed (300ms + margin)
|
window.__chatOpenT=setTimeout(done, 240); // fallback if transitionend is missed (140ms + margin)
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
async function openConvo(kind,id){
|
async function openConvo(kind,id){
|
||||||
const it=rowFor(kind,id)||{kind,id,name:'Conversation'};
|
const it=rowFor(kind,id)||{kind,id,name:'Conversation'};
|
||||||
|
|||||||
Reference in New Issue
Block a user