fix(rc+guest): coordinate/keyboard/UI for remote control; guest media + panel fixes (batch71)
Remote control (connect.html viewer): - Coordinate offset fixed: map clicks to the actual video CONTENT rect (object-fit letterbox-aware), not the element rect — cursor now lands where you click. - Keyboard: capture at document level while a session is live (the <video> lost focus on bar clicks → keys did nothing). Skips the chat input. - Control bar moved to bottom-right with tiny modern icons; video fills the viewport. - Smoother cursor (mousemove ~60/s). Guest meetings: - Guest mic inaudible + guest invisible in the participant list + outsider screen share not showing (#1/#8/#11): root cause was the SFU media→tile map keyed on identity==uid, but guests had a random LiveKit identity and a null signaling uid. Guests now carry ONE stable id across signaling (meeting-join guestId) and the LiveKit token identity, so their media attaches and they appear to everyone. - Guests can't add participants (#9) and don't see the transcript button (#12). - Search box in the schedule participant list (#10). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+13
-8
@@ -884,7 +884,7 @@
|
||||
<body>
|
||||
<script src="/icons.js?v=5"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@twemoji/api@15.1.0/dist/twemoji.min.js" crossorigin="anonymous"></script>
|
||||
<script>window.__BUILD='2026-07-10-batch70';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
|
||||
<script>window.__BUILD='2026-07-11-batch71';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
|
||||
// Render modern (Twemoji) emojis in place of the OS's flat ones. No-op if the CDN didn't load
|
||||
// (emojis stay as plain Unicode). (#5)
|
||||
function twemojify(el){ try{ if(el && window.twemoji) window.twemoji.parse(el, { folder:'svg', ext:'.svg' }); }catch(_){} }</script>
|
||||
@@ -1987,7 +1987,9 @@ function renderMeetPanel(){
|
||||
const list=meetParticipantsList();
|
||||
const isHostRow=pp=> pp.id==='__local' ? meetIsHost : pp.id===meetHostId; // YOUR row uses meetIsHost
|
||||
const head='<div class="mp-head"><b>Participants</b><span style="flex:1"></span>'+((meetIsHost&&meetPanelTab==='people')?'<button class="mp-muteall" title="Mute everyone">'+ic('micOff',14)+' Mute all</button>':'')+'<button class="mp-x" title="Close">'+ic('x',16)+'</button></div>';
|
||||
const tabs='<div class="mp-tabs"><button class="mp-tab'+(meetPanelTab==='people'?' on':'')+'" data-tab="people">In call ('+list.length+')</button><button class="mp-tab'+(meetPanelTab==='add'?' on':'')+'" data-tab="add">'+ic('userPlus',13)+' Add people</button></div>';
|
||||
// Guests can't add participants (#9) — show only the "In call" list, no Add-people tab.
|
||||
const isGuest=!!(ME&&ME.guest); if(isGuest && meetPanelTab==='add') meetPanelTab='people';
|
||||
const tabs='<div class="mp-tabs"><button class="mp-tab'+(meetPanelTab==='people'?' on':'')+'" data-tab="people">In call ('+list.length+')</button>'+(isGuest?'':'<button class="mp-tab'+(meetPanelTab==='add'?' on':'')+'" data-tab="add">'+ic('userPlus',13)+' Add people</button>')+'</div>';
|
||||
let body;
|
||||
if(meetPanelTab==='add'){
|
||||
// Group call → only that group's members may be added. 1:1/ad-hoc → all team contacts.
|
||||
@@ -2986,7 +2988,7 @@ async function sfuConnect(){
|
||||
const LK=await sfuLoadLib(); SFU.lib=LK;
|
||||
// Guests (external link joiners, not signed in) get an unauthenticated guest token for this room.
|
||||
const tk=(ME&&ME.guest)
|
||||
? await postJSON('/api/meetings/guest-token',{ room:meetRoom, name:ME.name })
|
||||
? await postJSON('/api/meetings/guest-token',{ room:meetRoom, name:ME.name, identity:ME.id }) // identity must match the guestId sent over signaling
|
||||
: await postJSON('/api/meetings/token',{ room:meetRoom }); // per-user, per-room join credential
|
||||
// adaptiveStream + dynacast BOTH off: we attach media via manual srcObject (not track.attach), so
|
||||
// LiveKit can't observe tile visibility. With dynacast on, a screen-share layer was paused unless a
|
||||
@@ -3169,7 +3171,8 @@ function openScheduleModal(gid, editMtg){
|
||||
+'<div class="sch-days hidden" id="schDays">'+DAY1.map((d,i)=>'<button type="button" class="day-chip" data-d="'+i+'" title="'+DAYW[i]+'">'+d+'</button>').join('')+'<button type="button" class="day-all" data-all="1">Everyday</button></div>'
|
||||
+'<label class="flbl">Description <span class="opt">(optional)</span></label><textarea id="schDesc" class="finput" rows="2" placeholder="What\'s this call about?"></textarea>'
|
||||
+'<label class="flbl">Invite participants</label>'
|
||||
+'<div class="gi-list" id="schPeople" style="max-height:20vh;overflow:auto">'+(CONTACTS.length?CONTACTS.map(c=>'<label class="chk"><input type="checkbox" value="'+pEsc(c.id)+'"'+(invitedIds.has(c.id)?' checked':'')+'><span class="mini-av" style="background:'+avColor(c.name)+'">'+pEsc(initials(c.name))+'</span><span class="mn">'+pEsc(c.name)+'</span></label>').join(''):'<div class="gi-noresult">No contacts to invite</div>')+'</div>'
|
||||
+(CONTACTS.length>6?'<div class="gi-search" style="margin:.15rem 0 .35rem"><input id="schPplSearch" placeholder="Search people…" autocomplete="off"></div>':'')
|
||||
+'<div class="gi-list" id="schPeople" style="max-height:20vh;overflow:auto">'+(CONTACTS.length?CONTACTS.map(c=>'<label class="chk" data-nm="'+pEsc((c.name||'').toLowerCase())+'"><input type="checkbox" value="'+pEsc(c.id)+'"'+(invitedIds.has(c.id)?' checked':'')+'><span class="mini-av" style="background:'+avColor(c.name)+'">'+pEsc(initials(c.name))+'</span><span class="mn">'+pEsc(c.name)+'</span></label>').join(''):'<div class="gi-noresult">No contacts to invite</div>')+'</div>'
|
||||
+'<label class="flbl" style="margin-top:.7rem">Invite by email <span class="opt">(guests — no Connect account needed)</span></label>'
|
||||
+'<div class="email-invite"><input id="schEmail" class="finput" type="email" placeholder="name@example.com" autocomplete="off"><button type="button" class="email-add" id="schEmailAdd">'+ic('userPlus',15)+' Add</button></div>'
|
||||
+'<div class="email-chips" id="schEmailChips"></div>'
|
||||
@@ -3225,6 +3228,8 @@ function openScheduleModal(gid, editMtg){
|
||||
$('schEmailAdd').onclick=addEmail;
|
||||
$('schEmail').addEventListener('keydown',e=>{ if(e.key==='Enter'){ e.preventDefault(); addEmail(); } });
|
||||
renderEmailChips();
|
||||
// #10: filter the participant list as you type.
|
||||
{ const ps=$('schPplSearch'); if(ps) ps.addEventListener('input',()=>{ const q=ps.value.trim().toLowerCase(); ov.querySelectorAll('#schPeople .chk').forEach(l=>{ l.style.display=(!q||(l.dataset.nm||'').includes(q))?'':'none'; }); }); }
|
||||
$('schSave').onclick=async()=>{
|
||||
const title=$('schTitle').value.trim();
|
||||
const desc=$('schDesc').value.trim();
|
||||
@@ -3258,14 +3263,14 @@ function renderCall(){
|
||||
+ '<button class="meet-ic'+(meetCam?'':' off')+'" id="meetCamBtn" title="'+(meetCam?'Turn camera off':'Turn camera on')+'">'+ic(meetCam?'video':'videoOff',20)+'</button>'
|
||||
+ '<button class="meet-ic" id="meetScreenBtn" title="Share screen">'+ic('monitor',20)+'</button>'
|
||||
+ '<button class="meet-ic host-only" id="meetRecBtn" title="Record meeting" style="display:none">'+ic('record',20)+'</button>'
|
||||
+ '<button class="meet-ic" id="meetTransBtn" title="Live transcript">'+ic('fileText',20)+'</button>'
|
||||
+ ((ME&&ME.guest)?'':'<button class="meet-ic" id="meetTransBtn" title="Live transcript">'+ic('fileText',20)+'</button>') // #12: transcript is a signed-in feature (guests can't download it)
|
||||
+ '<button class="meet-ic" id="meetPplBtn" title="Participants">'+ic('users',20)+'</button>'
|
||||
+ '<button class="meet-ic leave" id="meetLeaveBtn" title="Leave">'+ic('callEnd',20)+'</button></div></div>';
|
||||
document.getElementById('meetMicBtn').onclick=toggleMic;
|
||||
document.getElementById('meetCamBtn').onclick=toggleCam;
|
||||
document.getElementById('meetScreenBtn').onclick=toggleScreen;
|
||||
document.getElementById('meetRecBtn').onclick=toggleRecord;
|
||||
document.getElementById('meetTransBtn').onclick=toggleTranscribe;
|
||||
{ const tb=document.getElementById('meetTransBtn'); if(tb) tb.onclick=toggleTranscribe; }
|
||||
document.getElementById('meetPplBtn').onclick=toggleMeetPanel;
|
||||
document.getElementById('meetLeaveBtn').onclick=leaveMeeting;
|
||||
updateHostControls();
|
||||
@@ -3481,11 +3486,11 @@ async function enterMeeting(code, audioOnly){
|
||||
renderCallConnecting(); // branded "Connecting…" until the room is created/joined (esp. on slow links)
|
||||
meetWs=new WebSocket((location.protocol==='https:'?'wss://':'ws://')+location.host+'/ws');
|
||||
meetWs.onmessage=onMeetMsg;
|
||||
meetWs.onopen=()=>{ if(code){ meetRoom=code; renderCall(); meetSend({type:'meeting-join', room:code, name:(ME.name||ME.email||'Guest')}); } else { meetSend({type:'meeting-create'}); } };
|
||||
meetWs.onopen=()=>{ if(code){ meetRoom=code; renderCall(); meetSend({type:'meeting-join', room:code, name:(ME.name||ME.email||'Guest'), guestId:(ME&&ME.guest)?ME.id:undefined}); } else { meetSend({type:'meeting-create'}); } };
|
||||
}
|
||||
async function onMeetMsg(e){
|
||||
let m; try{ m=JSON.parse(e.data); }catch(_){ return; }
|
||||
if(m.type==='meeting-created'){ meetRoom=m.room; renderCall(); meetSend({type:'meeting-join', room:m.room, name:(ME.name||ME.email||'Guest')}); if(meetAnnounceGroup){ const g=meetAnnounceGroup; meetAnnounceGroup=null; try{ postJSON('/api/messages',{group:g, body:'📹 Started a group call — join with code '+m.room}); }catch(_){} } return; }
|
||||
if(m.type==='meeting-created'){ meetRoom=m.room; renderCall(); meetSend({type:'meeting-join', room:m.room, name:(ME.name||ME.email||'Guest'), guestId:(ME&&ME.guest)?ME.id:undefined}); if(meetAnnounceGroup){ const g=meetAnnounceGroup; meetAnnounceGroup=null; try{ postJSON('/api/messages',{group:g, body:'📹 Started a group call — join with code '+m.room}); }catch(_){} } return; }
|
||||
if(m.type==='meeting-joined'){
|
||||
meetMyId=m.peerId;
|
||||
if(m.isHost){ meetIsHost=true; meetHostId=meetMyId; } // host = the meeting creator (server-decided)
|
||||
|
||||
Reference in New Issue
Block a user