feat(meetings): guest join by link — external people can join without a Connect account (batch68)
- Server: POST /api/meetings/guest-token issues an unauthenticated LiveKit token for a room that is currently live or a valid scheduled meeting (throwaway guest identity). Mesh signaling already tolerates anonymous peers. - Client: opening /home?meet=CODE while signed out runs a lightweight GUEST mode — name prompt, then straight into the meeting with the full meeting client and the chat/sidebar chrome hidden. Signed-in users who open the link auto-join. - sfuConnect uses the guest token for guests. Guests get a friendly "left the meeting / rejoin" screen (no chat to fall back to). - Meeting "Add people" panel gains a "Copy invite link" (copyMeetingLink) that copies the guest link — foundation for the emailed invites in #4. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+81
-3
@@ -405,6 +405,13 @@
|
|||||||
.meet-panel .mp-scroll{flex:1;overflow:auto;}
|
.meet-panel .mp-scroll{flex:1;overflow:auto;}
|
||||||
.meet-panel .mp-invite-sticky{position:sticky;bottom:.4rem;width:calc(100% - 1rem);margin:.5rem;background:var(--blue);color:#fff;box-shadow:0 -6px 14px rgba(255,255,255,.06);} /* #4: keep Invite reachable while the list scrolls */
|
.meet-panel .mp-invite-sticky{position:sticky;bottom:.4rem;width:calc(100% - 1rem);margin:.5rem;background:var(--blue);color:#fff;box-shadow:0 -6px 14px rgba(255,255,255,.06);} /* #4: keep Invite reachable while the list scrolls */
|
||||||
.meet-panel .mp-list{padding:.4rem;}
|
.meet-panel .mp-list{padding:.4rem;}
|
||||||
|
.meet-panel .mp-invite-link{display:flex;align-items:center;gap:.55rem;margin:.5rem;padding:.6rem .7rem;background:var(--blue-soft);border:1px solid var(--line);border-radius:10px;}
|
||||||
|
.meet-panel .mp-invite-link svg{flex:0 0 auto;color:var(--blue);}
|
||||||
|
.meet-panel .mp-invite-link .mil-txt{flex:1;min-width:0;}
|
||||||
|
.meet-panel .mp-invite-link .mil-txt b{display:block;font-size:.82rem;color:var(--ink);}
|
||||||
|
.meet-panel .mp-invite-link .mil-txt span{font-size:.7rem;color:var(--muted);line-height:1.3;display:block;}
|
||||||
|
.meet-panel .mp-copylink{flex:0 0 auto;display:inline-flex;align-items:center;gap:.3rem;background:var(--blue);color:#fff;border:none;border-radius:8px;padding:.4rem .7rem;font-size:.78rem;font-weight:600;cursor:pointer;}
|
||||||
|
.meet-panel .mp-copylink:hover{filter:brightness(1.05);}
|
||||||
.meet-panel .chk{display:flex;align-items:center;gap:.5rem;padding:.4rem .5rem;border-radius:8px;cursor:pointer;font-size:.88rem;}
|
.meet-panel .chk{display:flex;align-items:center;gap:.5rem;padding:.4rem .5rem;border-radius:8px;cursor:pointer;font-size:.88rem;}
|
||||||
.meet-panel .chk:hover{background:#f6f8fb;}
|
.meet-panel .chk:hover{background:#f6f8fb;}
|
||||||
.meet-panel .chk input{width:16px;height:16px;flex:0 0 16px;accent-color:var(--blue);margin:0;}
|
.meet-panel .chk input{width:16px;height:16px;flex:0 0 16px;accent-color:var(--blue);margin:0;}
|
||||||
@@ -553,6 +560,14 @@
|
|||||||
.bubble .sender .snd-av{position:relative;width:20px;height:20px;flex:0 0 20px;border-radius:50%;display:grid;place-items:center;color:#334155;font-weight:800;font-size:.58rem;overflow:hidden;}
|
.bubble .sender .snd-av{position:relative;width:20px;height:20px;flex:0 0 20px;border-radius:50%;display:grid;place-items:center;color:#334155;font-weight:800;font-size:.58rem;overflow:hidden;}
|
||||||
.bubble .sender .snd-av img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;}
|
.bubble .sender .snd-av img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;}
|
||||||
.avatar.grp{border-radius:12px;font-size:1.15rem;}
|
.avatar.grp{border-radius:12px;font-size:1.15rem;}
|
||||||
|
/* Guest meeting mode: an external link-joiner sees ONLY the meeting — no rail, sidebar, or profile. */
|
||||||
|
body.guest-mode .rail, body.guest-mode #chatcol, body.guest-mode #hdrRight, body.guest-mode #navToggle, body.guest-mode .demo-note{display:none!important;}
|
||||||
|
body.guest-mode .content{width:100%;flex:1 1 auto;}
|
||||||
|
.guest-left{position:fixed;inset:0;background:#1F3B73;display:flex;align-items:center;justify-content:center;z-index:9900;padding:1rem;}
|
||||||
|
.guest-left .gl-card{background:#fff;border-radius:16px;padding:2rem 1.6rem;text-align:center;max-width:360px;width:100%;box-shadow:0 18px 44px rgba(0,0,0,.3);}
|
||||||
|
.guest-left .gl-card h2{margin:.2rem 0 .3rem;font-size:1.15rem;color:var(--ink);}
|
||||||
|
.guest-left .gl-card p{margin:0 0 1.1rem;color:var(--muted);font-size:.9rem;}
|
||||||
|
.guest-left .gl-card .bzc-ok{width:100%;justify-content:center;}
|
||||||
.modal-ov{position:fixed;inset:0;background:rgba(15,23,42,.45);display:flex;align-items:center;justify-content:center;z-index:9800;padding:1rem;}
|
.modal-ov{position:fixed;inset:0;background:rgba(15,23,42,.45);display:flex;align-items:center;justify-content:center;z-index:9800;padding:1rem;}
|
||||||
.modal{background:#fff;border-radius:16px;padding:1.4rem;max-width:380px;width:100%;box-shadow:0 18px 44px rgba(0,0,0,.3);max-height:calc(100vh - 2rem);max-height:calc(100dvh - 2rem);overflow-y:auto;}
|
.modal{background:#fff;border-radius:16px;padding:1.4rem;max-width:380px;width:100%;box-shadow:0 18px 44px rgba(0,0,0,.3);max-height:calc(100vh - 2rem);max-height:calc(100dvh - 2rem);overflow-y:auto;}
|
||||||
.modal h3{margin:0 0 .8rem;color:var(--blue);}
|
.modal h3{margin:0 0 .8rem;color:var(--blue);}
|
||||||
@@ -860,7 +875,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<script src="/icons.js?v=5"></script>
|
<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 src="https://cdn.jsdelivr.net/npm/@twemoji/api@15.1.0/dist/twemoji.min.js" crossorigin="anonymous"></script>
|
||||||
<script>window.__BUILD='2026-07-10-batch67';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
|
<script>window.__BUILD='2026-07-10-batch68';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
|
// 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)
|
// (emojis stay as plain Unicode). (#5)
|
||||||
function twemojify(el){ try{ if(el && window.twemoji) window.twemoji.parse(el, { folder:'svg', ext:'.svg' }); }catch(_){} }</script>
|
function twemojify(el){ try{ if(el && window.twemoji) window.twemoji.parse(el, { folder:'svg', ext:'.svg' }); }catch(_){} }</script>
|
||||||
@@ -1945,6 +1960,16 @@ function refreshMeetPanel(){ if(document.getElementById('meetPanel')) renderMeet
|
|||||||
// invite is re-enabled for them. When they actually join (matched by user id), they're cleared.
|
// invite is re-enabled for them. When they actually join (matched by user id), they're cleared.
|
||||||
function meetInviteJoined(uid){ const e=meetInvited.get(uid); if(e){ if(e.timer) clearTimeout(e.timer); meetInvited.delete(uid); refreshMeetPanel(); } }
|
function meetInviteJoined(uid){ const e=meetInvited.get(uid); if(e){ if(e.timer) clearTimeout(e.timer); meetInvited.delete(uid); refreshMeetPanel(); } }
|
||||||
function meetAddInvited(ids){ (ids||[]).forEach(uid=>{ const c=(CONTACTS||[]).find(x=>x.id===uid); const name=(c&&c.name)||'Someone'; const prev=meetInvited.get(uid); if(prev&&prev.timer) clearTimeout(prev.timer); const timer=setTimeout(()=>{ meetInvited.delete(uid); refreshMeetPanel(); }, 30000); meetInvited.set(uid,{name,timer}); }); refreshMeetPanel(); }
|
function meetAddInvited(ids){ (ids||[]).forEach(uid=>{ const c=(CONTACTS||[]).find(x=>x.id===uid); const name=(c&&c.name)||'Someone'; const prev=meetInvited.get(uid); if(prev&&prev.timer) clearTimeout(prev.timer); const timer=setTimeout(()=>{ meetInvited.delete(uid); refreshMeetPanel(); }, 30000); meetInvited.set(uid,{name,timer}); }); refreshMeetPanel(); }
|
||||||
|
// Guest invite link for the current meeting — anyone with it can join (guest token), even without a
|
||||||
|
// Connect account. Shared from the meeting's Add-people panel and emailed to invitees (#4).
|
||||||
|
function meetingLink(){ return location.origin+'/home?meet='+encodeURIComponent(meetRoom||''); }
|
||||||
|
function copyMeetingLink(){
|
||||||
|
if(!meetRoom){ toast('No active meeting'); return; }
|
||||||
|
const url=meetingLink(), done=()=>toast('Invite link copied');
|
||||||
|
try{ if(navigator.clipboard&&navigator.clipboard.writeText) return void navigator.clipboard.writeText(url).then(done).catch(()=>fallbackCopy(url,done)); }catch(_){}
|
||||||
|
fallbackCopy(url,done);
|
||||||
|
}
|
||||||
|
function fallbackCopy(text,cb){ try{ const ta=document.createElement('textarea'); ta.value=text; ta.style.position='fixed'; ta.style.opacity='0'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove(); cb&&cb(); }catch(_){ prompt('Copy this link:', text); } }
|
||||||
let meetPanelTab='people'; // 'people' | 'add'
|
let meetPanelTab='people'; // 'people' | 'add'
|
||||||
let _addPool=null; // cached candidates for "Add people" (group members for a group call)
|
let _addPool=null; // cached candidates for "Add people" (group members for a group call)
|
||||||
function toggleMeetPanel(){ const ex=document.getElementById('meetPanel'); if(ex){ ex.remove(); return; } const meet=document.querySelector('.meet'); if(!meet) return; meetPanelTab='people'; const p=document.createElement('div'); p.id='meetPanel'; p.className='meet-panel'; meet.appendChild(p); renderMeetPanel(); }
|
function toggleMeetPanel(){ const ex=document.getElementById('meetPanel'); if(ex){ ex.remove(); return; } const meet=document.querySelector('.meet'); if(!meet) return; meetPanelTab='people'; const p=document.createElement('div'); p.id='meetPanel'; p.className='meet-panel'; meet.appendChild(p); renderMeetPanel(); }
|
||||||
@@ -1965,7 +1990,8 @@ function renderMeetPanel(){
|
|||||||
const here=new Set(list.map(pp=>(pp.name||'').replace(/\s*\(you\)$/,'').trim().toLowerCase()));
|
const here=new Set(list.map(pp=>(pp.name||'').replace(/\s*\(you\)$/,'').trim().toLowerCase()));
|
||||||
const myName=((ME&&ME.name)||(ME&&ME.email)||'').trim().toLowerCase();
|
const myName=((ME&&ME.name)||(ME&&ME.email)||'').trim().toLowerCase();
|
||||||
const avail=pool.filter(c=>c.id!==(ME&&ME.id) && !hereUids.has(c.id) && (c.name||'').trim().toLowerCase()!==myName && !here.has((c.name||'').trim().toLowerCase()) && !meetInvited.has(c.id));
|
const avail=pool.filter(c=>c.id!==(ME&&ME.id) && !hereUids.has(c.id) && (c.name||'').trim().toLowerCase()!==myName && !here.has((c.name||'').trim().toLowerCase()) && !meetInvited.has(c.id));
|
||||||
body='<div class="mp-list">'+(avail.length?avail.map(c=>'<label class="chk"><input type="checkbox" value="'+pEsc(c.id)+'"><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">'+(inGroup&&_addPool===null?'Loading…':'No one left to add')+'</div>')+'</div>'+(avail.length?'<button class="gobtn mp-invite-sticky" id="mpInvite">Invite to call</button>':'');
|
body='<div class="mp-invite-link">'+ic('link',14)+'<div class="mil-txt"><b>Invite by link</b><span>Anyone with the link can join — even without a Connect account.</span></div><button class="mp-copylink" id="mpCopyLink">'+ic('copy',13)+' Copy</button></div>'
|
||||||
|
+'<div class="mp-list">'+(avail.length?avail.map(c=>'<label class="chk"><input type="checkbox" value="'+pEsc(c.id)+'"><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">'+(inGroup&&_addPool===null?'Loading…':'No one left to add')+'</div>')+'</div>'+(avail.length?'<button class="gobtn mp-invite-sticky" id="mpInvite">Invite to call</button>':'');
|
||||||
} else {
|
} else {
|
||||||
body='<div class="mp-list">'+list.map(pp=>'<div class="mp-row"><span class="mini-av" style="background:'+avColor(pp.name)+'">'+pEsc(initials(pp.name))+'</span><span class="mn">'+pEsc(pp.name)+'</span>'+(isHostRow(pp)?'<span class="host-tag">'+ic('crown',11)+' Host</span>':'')+((pp.id==='__local'?meetScreen:meetSharers.has(pp.id))?'<span class="pp-screen" title="Sharing screen">'+ic('monitor',13)+'</span>':'')+(meetMuted.get(pp.id)?'<span class="pp-mute">'+ic('micOff',13)+'</span>':'')+((meetIsHost&&pp.id!=='__local'&&!isHostRow(pp))?'<button class="mp-makehost" data-id="'+pEsc(pp.id)+'" title="Make host">'+ic('crown',14)+'</button>':'')+'</div>').join('')+'</div>'
|
body='<div class="mp-list">'+list.map(pp=>'<div class="mp-row"><span class="mini-av" style="background:'+avColor(pp.name)+'">'+pEsc(initials(pp.name))+'</span><span class="mn">'+pEsc(pp.name)+'</span>'+(isHostRow(pp)?'<span class="host-tag">'+ic('crown',11)+' Host</span>':'')+((pp.id==='__local'?meetScreen:meetSharers.has(pp.id))?'<span class="pp-screen" title="Sharing screen">'+ic('monitor',13)+'</span>':'')+(meetMuted.get(pp.id)?'<span class="pp-mute">'+ic('micOff',13)+'</span>':'')+((meetIsHost&&pp.id!=='__local'&&!isHostRow(pp))?'<button class="mp-makehost" data-id="'+pEsc(pp.id)+'" title="Make host">'+ic('crown',14)+'</button>':'')+'</div>').join('')+'</div>'
|
||||||
+(meetInvited.size?'<div class="mp-sec">Not joined yet</div><div class="mp-list">'+[...meetInvited.entries()].map(([uid,e])=>'<div class="mp-row waiting"><span class="mini-av" style="background:'+avColor(e.name)+'">'+pEsc(initials(e.name))+'</span><span class="mn">'+pEsc(e.name)+'</span><span class="pp-wait">'+ic('calendarClock',12)+' waiting…</span></div>').join('')+'</div>':'')
|
+(meetInvited.size?'<div class="mp-sec">Not joined yet</div><div class="mp-list">'+[...meetInvited.entries()].map(([uid,e])=>'<div class="mp-row waiting"><span class="mini-av" style="background:'+avColor(e.name)+'">'+pEsc(initials(e.name))+'</span><span class="mn">'+pEsc(e.name)+'</span><span class="pp-wait">'+ic('calendarClock',12)+' waiting…</span></div>').join('')+'</div>':'')
|
||||||
@@ -1977,6 +2003,7 @@ function renderMeetPanel(){
|
|||||||
const ma=p.querySelector('.mp-muteall'); if(ma) ma.onclick=()=>{ meetSend({type:'meeting-muteall'}); toast('Muted everyone'); };
|
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}); };
|
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(); });
|
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'); } };
|
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).
|
// 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; }
|
function peerIdForUid(uid){ for(const [pid,u] of meetPeerUids){ if(u===uid) return pid; } return null; }
|
||||||
async function sfuConnect(){
|
async function sfuConnect(){
|
||||||
const LK=await sfuLoadLib(); SFU.lib=LK;
|
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
|
// 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
|
// 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
|
// 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; }
|
if(meetWs){ try{ meetWs.close(); }catch(_){} meetWs=null; }
|
||||||
meetRoom=null; meetMyId=null; meetState='idle'; meetIsHost=false; meetHostId=null; meetRailLive(false);
|
meetRoom=null; meetMyId=null; meetState='idle'; meetIsHost=false; meetHostId=null; meetRailLive(false);
|
||||||
const ret=meetReturn; meetReturn=null; meetLeaving=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
|
if(ret){ switchTab('chat'); selectChat(ret.kind, ret.id); } // land back on the originating chat
|
||||||
else renderMeetingLobby();
|
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='<div class="gl-card"><img src="/mark-light.png" alt="" style="height:46px;width:auto;object-fit:contain;margin-bottom:.4rem" onerror="this.remove()">'
|
||||||
|
+'<h2>You’ve left the meeting</h2><p>Thanks for joining.</p>'
|
||||||
|
+(_guestRoom?'<button class="bzc-ok" id="glRejoin">Rejoin meeting</button>':'')+'</div>';
|
||||||
|
document.body.appendChild(el);
|
||||||
|
const rj=el.querySelector('#glRejoin'); if(rj) rj.onclick=()=>{ location.href='/home?meet='+encodeURIComponent(_guestRoom); };
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- Tabs (icon rail) ----------
|
// ---------- Tabs (icon rail) ----------
|
||||||
// Chat and Meeting are in-shell panels; Share and Connect load in the center panel via
|
// 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(){
|
(async function(){
|
||||||
let me=null;
|
let me=null;
|
||||||
try{ const r=await fetch('/api/me'); if(r.ok) me=await r.json(); }catch(_){}
|
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; }
|
if(!me){ await renderLogin(); document.getElementById('loading').style.display='none'; return; }
|
||||||
ME=me;
|
ME=me;
|
||||||
document.getElementById('hdrRight').innerHTML=bellHTML()+profileHTML(me);
|
document.getElementById('hdrRight').innerHTML=bellHTML()+profileHTML(me);
|
||||||
@@ -3719,7 +3767,37 @@ async function doRegister(){
|
|||||||
await loadSidebar(); renderChatPanel();
|
await loadSidebar(); renderChatPanel();
|
||||||
document.getElementById('loading').style.display='none'; // keep the branded splash up until the list is ready (no blank/stuck gap)
|
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='<div class="modal" style="max-width:380px;text-align:center">'
|
||||||
|
+'<img src="/mark-light.png" alt="" style="height:44px;width:auto;margin:0 auto .4rem;object-fit:contain" onerror="this.remove()">'
|
||||||
|
+'<h3 style="justify-content:center">Join the meeting</h3>'
|
||||||
|
+'<p class="bzc-msg" style="text-align:center">You’re joining meeting <b>'+pEsc(code)+'</b> as a guest.</p>'
|
||||||
|
+'<input id="guestName" placeholder="Your name" autocomplete="name" value="'+pEsc(savedName)+'" style="width:100%;box-sizing:border-box;border:1px solid var(--line);border-radius:9px;padding:.6rem .7rem;font-size:.95rem;margin:.2rem 0 .9rem">'
|
||||||
|
+'<button class="bzc-ok" id="guestGo" style="width:100%;justify-content:center">Join meeting</button></div>';
|
||||||
|
document.body.appendChild(ov);
|
||||||
|
const inp=ov.querySelector('#guestName'), go=ov.querySelector('#guestGo');
|
||||||
|
setTimeout(()=>{ try{ inp.focus(); }catch(_){} },50);
|
||||||
|
const join=async()=>{
|
||||||
|
const nm=(inp.value||'').trim()||'Guest';
|
||||||
|
try{ localStorage.setItem('bzc_guest_name', nm); }catch(_){}
|
||||||
|
ME={ id:'guest-'+Math.random().toString(36).slice(2,10), name:nm, email:'', guest:true, avatarUrl:null };
|
||||||
|
_guestRoom=code; ov.remove();
|
||||||
|
try{ await sfuInit(); }catch(_){}
|
||||||
|
switchTab('meeting'); enterMeeting(code);
|
||||||
|
};
|
||||||
|
go.onclick=join;
|
||||||
|
inp.addEventListener('keydown',e=>{ if(e.key==='Enter'){ e.preventDefault(); join(); } });
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -929,6 +929,23 @@ route('POST', '/api/meetings/token', async (req, res) => {
|
|||||||
json(res, 200, { token, url: LIVEKIT_URL, identity: u.id, name: u.name || u.email });
|
json(res, 200, { token, url: LIVEKIT_URL, identity: u.id, name: u.name || u.email });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// GUEST (no-login) LiveKit token — lets an external person invited by link join a meeting without a
|
||||||
|
// Connect account. Only minted for a room that is currently LIVE or a valid (not-ended) scheduled
|
||||||
|
// meeting, so a token can't be created for an arbitrary/expired code. Identity is a throwaway guest id.
|
||||||
|
route('POST', '/api/meetings/guest-token', async (req, res) => {
|
||||||
|
if (!LIVEKIT_ENABLED) return json(res, 501, { error: 'sfu not configured' });
|
||||||
|
const { room, name } = await readBody(req);
|
||||||
|
const rm = String(room || '').trim();
|
||||||
|
if (!/^\d{6}$/.test(rm)) return json(res, 400, { error: 'invalid room' });
|
||||||
|
const live = (() => { try { return require('./presence').meetingRooms.has(rm); } catch (_) { return false; } })();
|
||||||
|
const sched = (() => { try { const s = R.scheduledMeetings.byCode(rm); return !!(s && !s.ended_at); } catch (_) { return false; } })();
|
||||||
|
if (!live && !sched) return json(res, 404, { error: 'meeting not found or not active' });
|
||||||
|
const gid = 'guest-' + crypto.randomBytes(8).toString('hex');
|
||||||
|
const gname = String(name || 'Guest').slice(0, 60);
|
||||||
|
const token = livekitToken(gid, gname, rm, JSON.stringify({ guest: true }));
|
||||||
|
json(res, 200, { token, url: LIVEKIT_URL, identity: gid, name: gname });
|
||||||
|
});
|
||||||
|
|
||||||
// Decline an incoming 1:1 call: drops the caller, posts a "Call declined" line, clears the call.
|
// Decline an incoming 1:1 call: drops the caller, posts a "Call declined" line, clears the call.
|
||||||
route('POST', '/api/calls/decline', async (req, res) => {
|
route('POST', '/api/calls/decline', async (req, res) => {
|
||||||
const u = currentUser(req);
|
const u = currentUser(req);
|
||||||
|
|||||||
Reference in New Issue
Block a user