diff --git a/server/public/home.html b/server/public/home.html index 69d8e5a..316d039 100644 --- a/server/public/home.html +++ b/server/public/home.html @@ -1134,7 +1134,7 @@ - // fires at the START of the animation, so the CSS bottom-transition slides the composer up WITH the // keyboard (smooth). raw>innerHeight guards the rare device-pixel unit. function setKb(px){ px=Math.max(0,Math.round(px)); r.style.setProperty('--kb',px+'px'); document.body.classList.toggle('kb-open',px>4); var b=document.getElementById('msgs'); if(b) b.scrollTop=b.scrollHeight; } + window.__bzResetKb=function(){ setKb(0); }; // called on composer blur as a safety net (#1: keyboard closed but lift stayed, leaving empty space after reply-cancel) var KB=C.Plugins&&C.Plugins.Keyboard; if(KB && KB.addListener){ KB.addListener('keyboardWillShow', function(info){ var raw=(info&&info.keyboardHeight)||0; var kb = raw>window.innerHeight ? raw/(window.devicePixelRatio||1) : raw; setKb(kb); if(window.bzDbg) bzDbg('kbShow',{raw:raw, applied:Math.round(kb)}); }); KB.addListener('keyboardWillHide', function(){ setKb(0); }); + KB.addListener('keyboardDidHide', function(){ setKb(0); }); // belt-and-suspenders: some close paths only fire Did* } }catch(_){} })(); @@ -2634,22 +2636,26 @@ function openLightbox(src){ },{passive:false}); img.addEventListener('touchend',()=>{ pinchD=0; }); - const show=(i)=>{ if(!multi) return; idx=Math.max(0, Math.min(gallery.length-1, i)); const u=gallery[idx]; img.src=u; if(dl) dl.setAttribute('href',u); resetZoom(); }; // clamp (no wrap → no blank slide past the last) + const syncArrows=()=>{ if(!multi) return; const pB=ov.querySelector('.lb-prev'), nB=ov.querySelector('.lb-next'); if(pB) pB.style.visibility=idx<=0?'hidden':'visible'; if(nB) nB.style.visibility=idx>=gallery.length-1?'hidden':'visible'; }; + const show=(i)=>{ if(!multi) return; idx=Math.max(0, Math.min(gallery.length-1, i)); const u=gallery[idx]; img.src=u; if(dl) dl.setAttribute('href',u); resetZoom(); syncArrows(); }; // clamp (no wrap → no blank slide past the last) + hide the end arrow const close=()=>{ ov.remove(); document.removeEventListener('keydown', onKey); }; // #8: animated pager — the current image slides fully out in the swipe direction, then the next/prev - // image slides in from the OPPOSITE side (instead of an instant src swap). Stops at the first/last. - const pagerGo=(dir)=>{ // dir:+1 next (out to the left), -1 prev (out to the right) + // image slides in from the OPPOSITE side. Stops at the first/last. + const pagerGo=(dir)=>{ // dir:+1 next (current exits left, next enters from right), -1 prev (mirror) const target = idx + (dir>0?1:-1); if(!multi || target<0 || target>=gallery.length){ img.style.transition='transform .18s'; img.style.transform=''; return; } // at an edge → spring back const W=(window.innerWidth||400); let done=false; const finish=()=>{ if(done) return; done=true; img.removeEventListener('transitionend', finish); - show(target); // load the target image; resetZoom clears its transform - img.style.transition='none'; img.style.transform='translateX('+(dir>0?W:-W)+'px)'; // stage it off the opposite edge - requestAnimationFrame(()=>{ img.style.transition='transform .2s cubic-bezier(.3,.72,.3,1)'; img.style.transform='translateX(0)'; setTimeout(()=>{ if(img){ img.style.transition=''; img.style.transform=''; } ov.style.background=''; }, 220); }); + show(target); // load the target image; resetZoom clears its transform + img.style.transition='none'; img.style.transform='translateX('+(dir>0?W:-W)+'px)'; // stage off the OPPOSITE edge (next=right, prev=left) + void img.offsetWidth; // force that staged position to PAINT, else the browser animates from the exit side (wrong direction) + img.style.transition='transform .22s cubic-bezier(.3,.72,.3,1)'; img.style.transform='translateX(0)'; + setTimeout(()=>{ if(img){ img.style.transition=''; img.style.transform=''; } ov.style.background=''; }, 240); }; - img.style.transition='transform .16s ease-out'; img.style.transform='translateX('+(dir>0?-W:W)+'px)'; // current slides out - img.addEventListener('transitionend', finish); setTimeout(finish, 190); // fallback if transitionend misses + img.style.transition='transform .16s ease-out'; img.style.transform='translateX('+(dir>0?-W:W)+'px)'; // current slides out (next→left, prev→right) + img.addEventListener('transitionend', finish); setTimeout(finish, 180); // fallback if transitionend misses }; + syncArrows(); // initial: hide prev at first / next at last // #8: swipe DOWN (when not zoomed) to dismiss, like native photo viewers — drag follows the finger and // fades the backdrop; release past a threshold closes, otherwise it springs back. let dsy=0, dsx=0, dDrag=false; @@ -2775,7 +2781,7 @@ async function openConvo(kind,id){ const _d=getDraft(kind,id); if(_d){ inpEl.value=_d; autoGrow(inpEl); } // restore unsent draft inpEl.addEventListener('paste', onPaste); inpEl.addEventListener('input', ()=>{ maybeAutocorrect(inpEl); autoGrow(inpEl); setDraft(kind,id,inpEl.value); noteTyping(); }); // auto-correct common typos, keep the draft in sync + emit "typing…" - inpEl.addEventListener('blur', stopTyping); + inpEl.addEventListener('blur', ()=>{ stopTyping(); setTimeout(()=>{ const a=document.activeElement; const stillTyping=a&&(a.tagName==='TEXTAREA'||a.tagName==='INPUT'); if(!stillTyping && window.__bzResetKb) window.__bzResetKb(); }, 120); }); // #1: composer lost focus → keyboard is closing → drop the --kb lift so no empty gap remains inpEl.addEventListener('keydown', (e)=>{ if(e.key==='Enter' && !e.shiftKey){ if(mentionItems && mentionItems.length) return; e.preventDefault(); sendMessage(); } }); // Enter sends, Shift+Enter = newline // Desktop: LEFT-click a red-squiggled word → native spelling suggestions right there (the shell // pops a suggestions menu only if the clicked word is actually misspelled). No right-click needed.