fix: off-state colors, mobile audio route, iOS rail/PTR, in-place join, draggable bars (0.1.16/batch76)

Regressions I introduced, now fixed:
#8d Mic/Camera OFF went DIM — my new `.meet-ic.off` rule overrode the original RED.
    Scoped the lite style to the speaker button only; mic/cam OFF are red again.
#7  Guest "left the meeting" was unstyled (dark text on blue, no card): the card CSS was
    scoped to .guest-prejoin only. Shared with .guest-left. Rejoin no longer dead-ends —
    if the room is gone we say "This meeting has ended" and hide the button.

Reported:
#8d Speaker button no longer shows on desktop (devices already live under the mic ▾).
#8c/#8e Mobile speaker button now CYCLES Speaker → Earpiece → Bluetooth (icon follows the
    route); dropped the redundant "Audio devices" entry from the ⋮ menu.
#8a iOS gap under the bottom rail: the page rubber-band-bounced, exposing background beneath
    the fixed bar. overscroll-behavior:none + fixed body pins it.
#8b Pull-to-refresh now fires on iOS too (overscroll-behavior:contain on the lists so Safari's
    rubber band stops swallowing the gesture; scrollTop>2 tolerance for momentum).
#5  A meeting link no longer opens a whole new window: same-origin urls navigate the main
    window in the shell, and a link clicked in chat joins the meeting IN PLACE.

New observations:
1. The viewer's mic now starts MUTED on a screen session (and can actually be un/muted).
2. iOS lightbox close/download buttons moved below the Dynamic Island (safe-area insets).
3. Meeting chat: recipient picker moved to the BOTTOM next to the input; a private message
   auto-targets your reply back to that person; private vs everyone are visibly different
   (brand amber vs blue/neutral) and the compose area tints in private mode.
4. Meeting bar + screen-session bars are DRAGGABLE (position remembered) so they stop
   covering the shared screen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 22:24:36 +05:30
parent 48b6a4050a
commit e8c0b1889f
5 changed files with 203 additions and 50 deletions
+47 -3
View File
@@ -185,7 +185,9 @@ function connectWS(){
case 'session-ready': if(statusEl)statusEl.innerHTML='<img src="/loaders/loader-orbit.svg" width="20" height="20" style="vertical-align:-5px;margin-right:7px" alt="">Allowed — connecting…'; break;
case 'offer': await pc.setRemoteDescription(new RTCSessionDescription(m.sdp));
// Acquire the agent mic once; on renegotiation (e.g. customer unmutes) just answer.
if(!window.__mic){ try{ const mic=await navigator.mediaDevices.getUserMedia({audio:true}); window.__mic=mic; mic.getAudioTracks().forEach(t=>pc.addTrack(t,mic)); }catch(e){} }
// Acquire the agent mic MUTED by default — joining a screen session shouldn't open a hot mic on the
// customer without the agent choosing to speak (new #1). The Mic button unmutes it.
if(!window.__mic){ try{ const mic=await navigator.mediaDevices.getUserMedia({audio:true}); window.__mic=mic; mic.getAudioTracks().forEach(t=>{ t.enabled=false; pc.addTrack(t,mic); }); try{ setMicBtn(false); }catch(_){} }catch(e){} }
const ans=await pc.createAnswer(); await pc.setLocalDescription(ans);
ws.send(JSON.stringify({type:'answer',sessionId,sdp:pc.localDescription})); break;
case 'ice-candidate': if(m.candidate&&pc) await pc.addIceCandidate(new RTCIceCandidate(m.candidate)); break;
@@ -329,7 +331,7 @@ function buildBar(){
// Floats over the bottom-right corner with tiny icons, so the shared screen fills the whole viewport.
bar.style.cssText='position:fixed;right:16px;bottom:16px;z-index:2147483000;display:flex;flex-direction:row;gap:8px;align-items:center;background:rgba(15,23,42,.72);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);padding:7px 9px;border-radius:14px;box-shadow:0 8px 22px rgba(0,0,0,.35)';
const I=(n)=>(window.ic?window.ic(n,16):'');
const mic=_btn('micBtn',I('mic'),'Mic','#2563eb');
const mic=_btn('micBtn',I('micOff'),'Unmute','#6b7280'); // starts MUTED (new #1)
const ctrl=_btn('ctrlBtn',I('monitor'),'Control OFF — click their screen to take control','#6b7280');
const chat=_btn('chatBtn',I('chat'),'Chat','#334155');
const rec=_btn('recBtn','<svg viewBox="0 0 24 24" width="15" height="15"><circle cx="12" cy="12" r="7" fill="#ef4444"/></svg>','Record','#334155');
@@ -340,13 +342,22 @@ function buildBar(){
// Shrink from the default 48px round to a compact 38px so they read as "tiny icons".
[mic,ctrl,chat,rec,end].forEach(b=>{ b.style.width='38px'; b.style.height='38px'; b.style.boxShadow='none'; });
ctrl.onclick=()=>setEngaged(!rcEngaged);
makeBarDraggable(bar,'bzc_connectbar_pos'); // new #4: the bar hides part of the shared screen → move it
// A hint over the screen until they take control, so it's obvious how to start driving.
if(!document.getElementById('ctrlHint')){
const h=document.createElement('div'); h.id='ctrlHint';
h.textContent='Click the screen to take control · Esc to release';
document.body.appendChild(h);
}
mic.onclick=()=>{const m=window.__mic;if(!m)return;const t=m.getAudioTracks()[0];if(!t)return;t.enabled=!t.enabled;mic.title=t.enabled?'Mute':'Unmute';mic.innerHTML='<span style="display:inline-flex">'+I(t.enabled?'mic':'micOff')+'</span>';mic.style.background=t.enabled?'#2563eb':'#6b7280';};
mic.onclick=async()=>{
// If the mic wasn't acquired yet (e.g. the customer never renegotiated), get it now, then toggle.
if(!window.__mic){
try{ const s=await navigator.mediaDevices.getUserMedia({audio:true}); window.__mic=s; s.getAudioTracks().forEach(t=>{ t.enabled=false; if(pc) pc.addTrack(t,s); }); }
catch(e){ toast('Microphone permission was blocked.'); return; }
}
const t=window.__mic.getAudioTracks()[0]; if(!t) return;
t.enabled=!t.enabled; setMicBtn(t.enabled);
};
chat.onclick=toggleChat;
rec.onclick=()=>{ if(mediaRecorder&&mediaRecorder.state==='recording') stopRecording(); else startRecording(); };
end.onclick=()=>{ try{ws.send(JSON.stringify({type:'end-session',sessionId,reason:'agent-ended'}));}catch(_){} };
@@ -420,6 +431,39 @@ video.addEventListener('contextmenu',e=>e.preventDefault());
// in chat, never leaks keystrokes to the remote desktop). Click the screen (or the Control button) to
// take control; click away, press Esc, or leave the window to release it.
function rcTyping(){ const a=document.activeElement; return a && (a.tagName==='INPUT'||a.tagName==='TEXTAREA'); }
// new #4: drag the floating control bar anywhere — it otherwise sits over part of the shared screen.
// Drag from the bar background (not the buttons); position is remembered.
function makeBarDraggable(el,key){
if(!el||el._drag) return; el._drag=true;
let sx=0,sy=0,ox=0,oy=0,dragging=false;
const pt=(e)=>e.touches?e.touches[0]:e;
const clamp=()=>{ const w=el.offsetWidth,h=el.offsetHeight;
let x=parseFloat(el.style.left||'0'), y=parseFloat(el.style.top||'0');
x=Math.max(4,Math.min(x,window.innerWidth-w-4)); y=Math.max(4,Math.min(y,window.innerHeight-h-4));
el.style.left=x+'px'; el.style.top=y+'px'; };
const pin=(r)=>{ el.style.left=r.left+'px'; el.style.top=r.top+'px'; el.style.right='auto'; el.style.bottom='auto'; };
try{ const s=JSON.parse(localStorage.getItem(key)||'null'); if(s&&typeof s.x==='number'){ pin({left:s.x,top:s.y}); clamp(); } }catch(_){}
const move=(e)=>{ if(!dragging) return; const p=pt(e); el.style.left=(ox+(p.clientX-sx))+'px'; el.style.top=(oy+(p.clientY-sy))+'px'; clamp(); if(e.cancelable) e.preventDefault(); };
const up=()=>{ if(!dragging) return; dragging=false; el.style.opacity='';
document.removeEventListener('mousemove',move); document.removeEventListener('mouseup',up);
document.removeEventListener('touchmove',move); document.removeEventListener('touchend',up);
try{ localStorage.setItem(key, JSON.stringify({x:parseFloat(el.style.left), y:parseFloat(el.style.top)})); }catch(_){} };
const down=(e)=>{ if(e.target.closest('button,select,input,a')) return;
const r=el.getBoundingClientRect(); pin(r);
const p=pt(e); sx=p.clientX; sy=p.clientY; ox=r.left; oy=r.top; dragging=true; el.style.opacity='.92';
document.addEventListener('mousemove',move); document.addEventListener('mouseup',up);
document.addEventListener('touchmove',move,{passive:false}); document.addEventListener('touchend',up);
if(e.cancelable) e.preventDefault(); };
el.addEventListener('mousedown',down); el.addEventListener('touchstart',down,{passive:false});
el.style.cursor='move'; el.title='Drag to move';
}
// Mic button state (kept global so the offer handler can set it once the mic is acquired, muted).
function setMicBtn(on){
const b=document.getElementById('micBtn'); if(!b) return;
b.title=on?'Mute':'Unmute';
b.innerHTML='<span style="display:inline-flex">'+(window.ic?window.ic(on?'mic':'micOff',16):'')+'</span>';
b.style.background=on?'#2563eb':'#6b7280';
}
let rcEngaged=false;
function setEngaged(on){
const next=!!on; if(next===rcEngaged) return;