fix: guest pre-join, post-admit lobby, audio device menu, RC keyboard (0.1.15/batch74)

1. Guest pre-join redesigned: brand backdrop, live camera preview, mic/cam toggles
   applied on entry, initials avatar, name required, "host may admit you" note.
2. Post-admit bug: the guest stayed on "Waiting for the host…" forever — the lobby
   screen had replaced the call UI and meeting-joined never re-rendered it. Now it
   rebuilds the call on admission (_inLobby).
3. Audio devices: dropped the standalone headphones button. The MIC now has a ▾ caret
   opening one Teams-style menu with Speaker + Microphone sections (radio-selected);
   speaker uses setSinkId/LiveKit switchActiveDevice, mic switches the live input.
   Mobile gets a speakerphone toggle that prefers a connected BT/headset when off.
4. Remote-control keyboard:
   - Injector now maps the PHYSICAL key (KeyboardEvent.code) instead of the character,
     so Shift+1 types "!" etc. Character mapping was why typing "performed differently".
   - Keys reach the sharer ONLY while control is ENGAGED (window focused AND you clicked
     their screen). Minimised/unfocused/chat typing stays local. Esc or clicking away
     releases; modifiers are released on disengage so nothing sticks.
   - Explicit control icons: viewer gets a Control ON/OFF button (green when engaged) +
     an on-screen hint; the SHARER gets a control icon beside mic/chat to allow/stop
     access at a glance, synced with the consent dialog and banner.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 13:18:01 +05:30
parent 82fa4a24d3
commit 94b8fac32f
5 changed files with 255 additions and 41 deletions
+20 -4
View File
@@ -226,7 +226,7 @@ function stopCustTranscription(){ crecogActive=false; if(crecog){ try{crecog.sto
// after the user explicitly taps "Allow control", and only inside the desktop app. In a plain browser
// there's no OS injection possible, so it stays view-only. The user can stop control at any time.
let rcDesktop=false, rcAllowed=false, rcPrompted=false;
try{ window.addEventListener('message',(e)=>{ if(e.origin!==location.origin) return; const d=e.data||{}; if(d.type==='rc-pong') rcDesktop=!!d.desktop; }); }catch(_){}
try{ window.addEventListener('message',(e)=>{ if(e.origin!==location.origin) return; const d=e.data||{}; if(d.type==='rc-pong'){ rcDesktop=!!d.desktop; try{ updateRcBtn(); }catch(_){} } }); }catch(_){}
try{ if(window.parent && window.parent!==window) window.parent.postMessage({type:'rc-ping'}, location.origin); }catch(_){}
function rcPost(msg){ try{ if(window.parent && window.parent!==window) window.parent.postMessage(msg, location.origin); }catch(_){} }
function rcOnInput(data){
@@ -241,9 +241,22 @@ function showControlConsent(){
const el=document.createElement('div'); el.id='rcConsent'; el.className='rc-consent';
el.innerHTML='<div class="rc-card"><div class="rc-h">Allow remote control?</div><p>Your agent is asking to control your mouse &amp; keyboard to help you. You stay in charge — stop anytime.</p><div class="rc-btns"><button id="rcDeny" class="rc-deny">Not now</button><button id="rcAllow" class="rc-allow">Allow control</button></div></div>';
document.body.appendChild(el);
el.querySelector('#rcAllow').onclick=()=>{ rcAllowed=true; rcPost({type:'rc-arm', on:true}); el.remove(); showControlBanner(); };
el.querySelector('#rcAllow').onclick=()=>{ el.remove(); rcGrantControl(); };
el.querySelector('#rcDeny').onclick=()=>{ el.remove(); };
}
// Grant / revoke control. Both the consent dialog and the bar's control icon route through here, so the
// icon, banner and the shell's injection gate always agree.
function rcGrantControl(){
if(!rcDesktop){ setStatus('Remote control needs the Biz Connect desktop app. Your agent can still see your screen.'); return; }
rcAllowed=true; rcPost({type:'rc-arm', on:true}); showControlBanner(); updateRcBtn();
}
function rcStopControl(){ rcAllowed=false; rcPrompted=false; rcPost({type:'rc-arm', on:false}); const b=document.getElementById('rcBanner'); if(b) b.remove(); updateRcBtn(); }
function updateRcBtn(){
const b=document.getElementById('rcBtn'); if(!b) return;
b.style.background=rcAllowed?'#16a34a':'#6b7280';
b.style.opacity=rcDesktop?'1':'.55';
b.title=!rcDesktop ? 'Remote control needs the desktop app' : (rcAllowed?'Remote control is ON — tap to stop':'Remote control is OFF — tap to allow');
}
function showControlBanner(){
if(document.getElementById('rcBanner')) return;
const el=document.createElement('div'); el.id='rcBanner'; el.className='rc-banner';
@@ -251,7 +264,6 @@ function showControlBanner(){
document.body.appendChild(el);
el.querySelector('#rcStop').onclick=rcStopControl;
}
function rcStopControl(){ rcAllowed=false; rcPrompted=false; rcPost({type:'rc-arm', on:false}); const b=document.getElementById('rcBanner'); if(b) b.remove(); }
let recTimerInt=null, recStartTs=0;
function fmtElapsed(ms){const s=Math.max(0,Math.floor(ms/1000));return String(Math.floor(s/60)).padStart(2,'0')+':'+String(s%60).padStart(2,'0');}
function recNotice(on){
@@ -293,10 +305,14 @@ function buildBar(){
const bar=document.createElement('div'); bar.id='sessionBar';
bar.style.cssText='position:fixed;right:18px;bottom:18px;z-index:2147483000;display:flex;gap:10px;align-items:center;background:rgba(15,23,42,.94);padding:8px 12px;border-radius:16px;box-shadow:0 10px 28px rgba(0,0,0,.35)';
const mic=_btn('micBtn',SVG_MICOFF,'Muted','#6b7280');
// Remote-control access, right next to mic/chat, so the customer can see AND flip it at a glance.
const rcb=_btn('rcBtn',(window.ic?window.ic('monitor',18):''),'Remote control is OFF','#6b7280');
const chat=_btn('chatBtn',SVG_CHAT,'Chat','#475569');
const end=_btn('endBtn2',SVG_END,'End','#dc2626');
bar.appendChild(mic);bar.appendChild(chat);bar.appendChild(end);
bar.appendChild(mic);bar.appendChild(rcb);bar.appendChild(chat);bar.appendChild(end);
document.body.appendChild(bar);
rcb.onclick=()=>{ if(rcAllowed) rcStopControl(); else rcGrantControl(); };
updateRcBtn();
const setMic=(on)=>{mic.title=on?'Mute':'Unmute';mic.innerHTML='<span style="display:inline-flex">'+(on?SVG_MIC:SVG_MICOFF)+'</span>';mic.style.background=on?'#2563eb':'#6b7280';};
mic.onclick=async()=>{
if(!window.__mic){