diff --git a/server/public/home.html b/server/public/home.html index 4b010c9..b3cefb9 100644 --- a/server/public/home.html +++ b/server/public/home.html @@ -3368,13 +3368,31 @@ async function openConvo(kind,id){ const ab=e.target.closest('.react-btn'); if(ab){ openEmojiForReact(ab.dataset.id, ab); return; } const ch=e.target.closest('.react-chip'); if(ch){ reactToMessage(ch.dataset.id, ch.dataset.emoji); return; } const sb=e.target.closest('.seenby,.rcpt.grp'); if(sb){ const bub=sb.closest('.bubble'); const mid=bub&&bub.dataset.id; const msg=(mid&&THREAD.find(x=>x.id===mid))||{ seenBy:(sb.dataset.seen||'').split('|').filter(Boolean) }; showSeenByModal(msg); return; } - // Tap-to-reveal (mobile): a tap on the bubble body (not an action) reveals its reply/react/delete - // icons; the action only fires on a SECOND tap once they're shown (icons are pointer-events:none - // until revealed). Tapping elsewhere hides them. - const bub=e.target.closest('.bubble'); const already=bub&&bub.classList.contains('show-actions'); + // #9 (mobile): a single tap now performs the PRIMARY action (open image/file, jump to a reply — all + // handled above). The reply/react/more actions are revealed by LONG-PRESS (handler below), not by tap. + // A tap that reaches here (empty space or a plain text bubble) just DISMISSES any revealed action bar. box.querySelectorAll('.bubble.show-actions').forEach(b=>b.classList.remove('show-actions')); - if(bub && !already && !bub.classList.contains('deleted')) bub.classList.add('show-actions'); }); + // #9 (mobile): LONG-PRESS a message to reveal its actions (reply / react / more). Touch only — desktop + // still reveals on hover. Movement or an early release cancels; when it fires we suppress the trailing tap + // so the long-press doesn't also open an image / jump a reply. + if(box){ + let lpT=null, lpBub=null, lpX=0, lpY=0, lpFired=false; + const lpCancel=()=>{ if(lpT){ clearTimeout(lpT); lpT=null; } lpBub=null; }; + box.addEventListener('touchstart',(e)=>{ + if(e.touches.length!==1){ lpCancel(); return; } + const bub=e.target.closest('.bubble'); if(!bub || bub.classList.contains('deleted')){ lpCancel(); return; } + lpBub=bub; lpX=e.touches[0].clientX; lpY=e.touches[0].clientY; lpFired=false; + lpT=setTimeout(()=>{ lpT=null; if(!lpBub) return; + box.querySelectorAll('.bubble.show-actions').forEach(b=>{ if(b!==lpBub) b.classList.remove('show-actions'); }); + lpBub.classList.add('show-actions'); lpFired=true; + try{ if(navigator.vibrate) navigator.vibrate(12); }catch(_){} + }, 420); + },{passive:true}); + box.addEventListener('touchmove',(e)=>{ if(lpT && e.touches.length===1){ const t=e.touches[0]; if(Math.abs(t.clientX-lpX)>10||Math.abs(t.clientY-lpY)>10) lpCancel(); } },{passive:true}); + box.addEventListener('touchend',(e)=>{ if(lpFired){ try{ e.preventDefault(); }catch(_){} lpFired=false; lpCancel(); return; } lpCancel(); },{passive:false}); + box.addEventListener('touchcancel',lpCancel,{passive:true}); + } if(box) box.addEventListener('scroll', onMsgsScroll); if(box){ const _rel=()=>{ _forcePinOpen=false; _openScrolled=true; _openPinTimers.forEach(clearTimeout); _openPinTimers.length=0; }; box.addEventListener('touchmove', _rel, {passive:true}); box.addEventListener('wheel', _rel, {passive:true}); } // the user's first real scroll gesture hands control back: stop force-gluing to the newest AND cancel the pending open-pins so scrolling up for older history isn't yanked back down if(box) enablePullRefresh(box, loadOlder); // pull down at the top loads OLDER history (or no-ops); never jumps to the newest/bottom