#9: mobile — single tap acts, long-press reveals message actions

On touch, tapping a message used to reveal the reply/react/more bar (so the
action needed a second tap). Now a single tap performs the primary action
(open image/file, jump to a reply), and the action bar is revealed by a
~420ms long-press (with a small haptic); movement or an early release cancels,
and a fired long-press suppresses the trailing tap. A tap elsewhere dismisses
the revealed bar. Desktop hover behaviour is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 22:51:35 +05:30
parent c1b67d38d3
commit 9017d2ff25
+23 -5
View File
@@ -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 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 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; } 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 // #9 (mobile): a single tap now performs the PRIMARY action (open image/file, jump to a reply — all
// icons; the action only fires on a SECOND tap once they're shown (icons are pointer-events:none // handled above). The reply/react/more actions are revealed by LONG-PRESS (handler below), not by tap.
// until revealed). Tapping elsewhere hides them. // A tap that reaches here (empty space or a plain text bubble) just DISMISSES any revealed action bar.
const bub=e.target.closest('.bubble'); const already=bub&&bub.classList.contains('show-actions');
box.querySelectorAll('.bubble.show-actions').forEach(b=>b.classList.remove('show-actions')); 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) 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){ 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 if(box) enablePullRefresh(box, loadOlder); // pull down at the top loads OLDER history (or no-ops); never jumps to the newest/bottom