diff --git a/server/public/home.html b/server/public/home.html index cf6c0ef..d33b2d3 100644 --- a/server/public/home.html +++ b/server/public/home.html @@ -367,6 +367,14 @@ .composer-box:focus-within{border-color:var(--blue);} .composer-row{display:flex;align-items:flex-end;gap:.15rem;padding:.15rem .3rem;} .composer-row input,.composer-row textarea{flex:1;min-width:0;box-sizing:border-box;border:none;background:transparent;padding:.5rem .4rem;margin:0;font-size:16px;color:var(--ink);font-family:inherit;resize:none;line-height:1.4;max-height:140px;overflow-y:hidden;display:block;scrollbar-width:thin;} /* 16px UNCONDITIONALLY (not just in the mobile @media) — anything under 16px makes iOS Safari/PWA zoom the page on focus, and the mobile media query doesn't match on iPad / landscape. */ + /* #1: iOS zooms + stretches the whole page when a focused field is < 16px. maximum-scale=1 is unreliable + (ignored by iOS Safari/PWA) and the width-based mobile @media misses iPad / landscape. Target ALL touch + devices by POINTER TYPE instead — the exact set that has the focus-zoom problem — and force 16px on every + focusable field so selecting a text box never zooms. Desktop (fine pointer) is untouched. */ + @media (pointer: coarse){ + input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=color]):not([type=file]), + textarea, select{ font-size:16px !important; } + } .composer-row input:focus,.composer-row textarea:focus{outline:none;} .ic-btn{border:none;background:transparent;color:var(--muted);cursor:pointer;width:36px;height:36px;border-radius:10px;display:grid;place-items:center;flex:0 0 auto;} .ic-btn:hover{color:var(--blue);background:var(--blue-soft);} @@ -3106,7 +3114,7 @@ function openLightbox(src){ },{passive:false}); img.addEventListener('touchend',()=>{ pinchD=0; }); - 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 syncArrows=()=>{ if(!multi) return; const pB=ov.querySelector('.lb-prev'), nB=ov.querySelector('.lb-next'); if(pB) pB.style.display=idx<=0?'none':''; if(nB) nB.style.display=idx>=gallery.length-1?'none':''; }; // #6: fully hide (not just invisible) the arrow with nothing beyond it — no left arrow at the first image, no right arrow at the last 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 @@ -4182,6 +4190,17 @@ function playPing(){ tone(1174.7, 0.11, 0.40, 0.34); // D6 — rises, rings out -> "ti-doo" }catch(_){} } +// #7: a soft, friendly two-note chime when a NEW participant joins the meeting (distinct from the message ping). +function playJoinChime(){ + try{ + _audioCtx=_audioCtx||new (window.AudioContext||window.webkitAudioContext)(); + if(_audioCtx.state==='suspended') _audioCtx.resume(); + const t=_audioCtx.currentTime; + const tone=(f,start,dur,peak)=>{ const o=_audioCtx.createOscillator(), g=_audioCtx.createGain(); o.type='sine'; o.frequency.value=f; g.gain.setValueAtTime(0.0001,t+start); g.gain.exponentialRampToValueAtTime(peak,t+start+0.02); g.gain.exponentialRampToValueAtTime(0.0001,t+start+dur); o.connect(g); g.connect(_audioCtx.destination); o.start(t+start); o.stop(t+start+dur+0.03); }; + tone(587.33, 0.00, 0.42, 0.15); // D5 + tone(880.0, 0.12, 0.55, 0.14); // A5 — gentle rising "doo-dee" + }catch(_){} +} // Continuous incoming-call ring. A soft, soothing bell chime — a gentle ascending arpeggio // (C major) of pure sine tones with an octave shimmer and a long smooth decay, so it rings // rather than buzzes. Ref-counted: several pending invites share one ring; it stops on the last close. @@ -5609,6 +5628,7 @@ async function onMeetMsg(e){ if(m.type==='meeting-ended'){ toast(m.reason==='unanswered'?'No answer':'Call ended'); leaveMeeting(true); return; } // 1:1 hangup / host ended / unanswered if(m.type==='meeting-peer-joined'){ stopRingback(); removeWaitingTile(); _dmCallWaiting=null; // #17/#7: they answered → stop ring + drop the ringing tile + try{ playJoinChime(); toast((m.name||'Someone')+' joined the call'); }catch(_){} // #7: sound + a brief who-joined note meetNames.set(m.peerId,m.name); if(m.avatar) meetAvatars.set(m.peerId,m.avatar); if(m.uid){ meetPeerUids.set(m.peerId,m.uid); meetInviteJoined(m.uid); dropDupPeers(m.uid, m.peerId); } const pc=meetMakePeer(m.peerId,m.name); // I'm an existing peer → I OFFER to the newcomer (carries my screen) if(pc){ try{ const offer=await pc.createOffer(); await pc.setLocalDescription(offer); meetSend({type:'meeting-signal',to:m.peerId,data:{sdp:pc.localDescription}}); }catch(_){} } // (SFU: LiveKit handles media, no offer)