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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user