':'')
@@ -1977,6 +2003,7 @@ function renderMeetPanel(){
const ma=p.querySelector('.mp-muteall'); if(ma) ma.onclick=()=>{ meetSend({type:'meeting-muteall'}); toast('Muted everyone'); };
const mm=p.querySelector('#mpMulti'); if(mm) mm.onchange=()=>{ meetMultiShare=mm.checked; meetSend({type:'meeting-sharemode', multi:meetMultiShare}); };
p.querySelectorAll('.mp-makehost').forEach(b=>b.onclick=()=>{ meetSend({type:'meeting-host', to:b.dataset.id}); meetHostId=b.dataset.id; meetIsHost=false; renderMeetPanel(); });
+ const cpl=p.querySelector('#mpCopyLink'); if(cpl) cpl.onclick=()=>copyMeetingLink();
const inv=p.querySelector('#mpInvite'); if(inv) inv.onclick=async()=>{ const ids=[...p.querySelectorAll('input:checked')].map(i=>i.value); if(!ids.length){ toast('Pick people to invite'); return; } try{ await postJSON('/api/calls/invite',{ room:meetRoom, userIds:ids }); meetAddInvited(ids); meetPanelTab='people'; toast(ids.length===1?'Waiting for them to join…':'Waiting for '+ids.length+' people to join…'); renderMeetPanel(); }catch(e){ toast(e.message||'Could not invite'); } };
}
// Add people to the current call (from the in-call bar).
@@ -2948,7 +2975,10 @@ function sfuLoadLib(){ return new Promise((res,rej)=>{ if(window.LivekitClient)
function peerIdForUid(uid){ for(const [pid,u] of meetPeerUids){ if(u===uid) return pid; } return null; }
async function sfuConnect(){
const LK=await sfuLoadLib(); SFU.lib=LK;
- const tk=await postJSON('/api/meetings/token',{ room:meetRoom }); // per-user, per-room join credential
+ // 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/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
// camera track was also flowing → the screen showed blank / only when the camera was on, and was
@@ -3545,9 +3575,22 @@ function leaveMeeting(forced){
if(meetWs){ try{ meetWs.close(); }catch(_){} meetWs=null; }
meetRoom=null; meetMyId=null; meetState='idle'; meetIsHost=false; meetHostId=null; meetRailLive(false);
const ret=meetReturn; meetReturn=null; meetLeaving=false;
+ if(ME&&ME.guest){ renderGuestLeft(); return; } // guests have no chat to return to — show a leave screen
if(ret){ switchTab('chat'); selectChat(ret.kind, ret.id); } // land back on the originating chat
else renderMeetingLobby();
}
+// Guest post-call screen: they can't drop back into a chat/sidebar (there is none), so show a simple
+// "you've left" card with a one-click Rejoin.
+let _guestRoom=null;
+function renderGuestLeft(){
+ document.querySelectorAll('.guest-left').forEach(x=>x.remove());
+ const el=document.createElement('div'); el.className='guest-left';
+ el.innerHTML='
'
+ +'
You’ve left the meeting
Thanks for joining.
'
+ +(_guestRoom?'':'')+'
';
+ document.body.appendChild(el);
+ const rj=el.querySelector('#glRejoin'); if(rj) rj.onclick=()=>{ location.href='/home?meet='+encodeURIComponent(_guestRoom); };
+}
// ---------- Tabs (icon rail) ----------
// Chat and Meeting are in-shell panels; Share and Connect load in the center panel via
@@ -3695,6 +3738,11 @@ async function doRegister(){
(async function(){
let me=null;
try{ const r=await fetch('/api/me'); if(r.ok) me=await r.json(); }catch(_){}
+ // Guest meeting join: someone opened a shared meeting link (…/home?meet=CODE or /j/CODE). If they
+ // aren't signed in, run in GUEST mode — no chat/sidebar, just a name prompt then straight into the
+ // call. Signed-in users fall through and auto-join after their normal boot (handled below).
+ let _meet=null; try{ _meet=(new URLSearchParams(location.search)).get('meet'); }catch(_){}
+ if(_meet && /^\d{6}$/.test(_meet) && !me){ document.getElementById('loading').style.display='none'; return startGuestMeeting(_meet); }
if(!me){ await renderLogin(); document.getElementById('loading').style.display='none'; return; }
ME=me;
document.getElementById('hdrRight').innerHTML=bellHTML()+profileHTML(me);
@@ -3719,7 +3767,37 @@ async function doRegister(){
await loadSidebar(); renderChatPanel();
document.getElementById('loading').style.display='none'; // keep the branded splash up until the list is ready (no blank/stuck gap)
}
+ // Signed-in user opened a meeting link → jump straight into that meeting.
+ if(_meet && /^\d{6}$/.test(_meet)){ try{ history.replaceState(null,'','/home'); }catch(_){} switchTab('meeting'); enterMeeting(_meet); }
})();
+
+// GUEST meeting: a lightweight pre-join (name) then join the call with a throwaway guest identity —
+// reuses the full meeting client (LiveKit/mesh, tiles, mic/cam) with the chat/sidebar chrome hidden.
+async function startGuestMeeting(code){
+ document.body.classList.add('guest-mode');
+ try{ history.replaceState(null,'','/home?meet='+code); }catch(_){}
+ const savedName=(()=>{ try{ return localStorage.getItem('bzc_guest_name')||''; }catch(_){ return ''; } })();
+ const ov=document.createElement('div'); ov.className='modal-ov'; ov.id='guestJoin'; ov.style.zIndex='9900';
+ ov.innerHTML='