diff --git a/server/public/home.html b/server/public/home.html index 32569b6..3fd4cff 100644 --- a/server/public/home.html +++ b/server/public/home.html @@ -4469,6 +4469,7 @@ let meetReturn=null; // {kind:'dm'|'group', id} — chat to land on when const meetVU=new Map(); // peerId|'__local' -> {ctx,analyser,data,raf} active-speaker meters let MEET_ICE={ iceServers:[{ urls:'stun:stun.l.google.com:19302' }] }; let meetMic=true, meetCam=true; +let _pendingJoinMic=false, _pendingJoinCam=false; // #4a: pre-join (lobby) mic/cam choices, applied once actually admitted let meetIsHost=false, meetHostId=null; // host = the meeting creator (transferable by the host only) let meetScreen=false, meetScreenStream=null; // am I sharing my screen + the display stream const meetSharers=new Set(); // peerIds of OTHERS currently sharing their screen @@ -5541,6 +5542,10 @@ async function onMeetMsg(e){ // for the UI only. Media is native; tell peers our mic is live. meetSend({type:'meeting-state', muted:!meetMic, camOff:!meetCam}); // tell existing peers my state if(meetIsHost) meetSend({type:'meeting-host', to:meetMyId}); // announce host so others know + // #4a: now that we're actually in the call (post-admission, media connected), honor the mic/cam the + // user turned on at the pre-join/lobby screen — so they don't have to re-tap after being admitted. + if(_pendingJoinMic){ _pendingJoinMic=false; if(!meetMic){ try{ await toggleMic(); }catch(_){} } } + if(_pendingJoinCam){ _pendingJoinCam=false; if(!meetCam){ try{ await toggleCam(); }catch(_){} } } refreshMeetPanel(); updateHostControls(); return; } @@ -5570,7 +5575,7 @@ async function onMeetMsg(e){ if(m.type==='meeting-peer-screen'){ if(m.on) meetSharers.add(m.from); else meetSharers.delete(m.from); setTileScreen(m.from, !!m.on); refreshMeetPanel(); return; } if(m.type==='meeting-sharemode'){ meetMultiShare=!!m.multi; refreshMeetPanel(); return; } if(m.type==='meeting-muteall'){ if(meetMic && meetLocalStream){ meetMic=false; meetLocalStream.getAudioTracks().forEach(t=>t.enabled=false); updateMicBtn(); setTileMute('__local', true); meetSend({type:'meeting-state', muted:true, camOff:!meetCam}); } toast('You were muted by the host'); return; } - if(m.type==='meeting-peer-left'){ const p=meetPeers.get(m.peerId); if(p){ try{p.pc.close();}catch(_){} meetPeers.delete(m.peerId);} if(SFU.on) sfuDropPeer(m.peerId); meetSharers.delete(m.peerId); removeTile(m.peerId); refreshMeetPanel(); return; } + if(m.type==='meeting-peer-left'){ const p=meetPeers.get(m.peerId); if(p){ try{p.pc.close();}catch(_){} meetPeers.delete(m.peerId);} if(SFU.on) sfuDropPeer(m.peerId); meetSharers.delete(m.peerId); meetPeerUids.delete(m.peerId); meetNames.delete(m.peerId); meetAvatars.delete(m.peerId); removeTile(m.peerId); refreshMeetPanel(); return; } // #4b: drop uid/name too, else they stay in hereUids and never reappear under "Add people" if(m.type==='meeting-signal'){ const from=m.from, d=m.data||{}; if(d.sdp){ @@ -6071,9 +6076,10 @@ async function startGuestMeeting(code){ ME={ id:'guest-'+Math.random().toString(36).slice(2,10), name:nm, email:'', guest:true, avatarUrl:null }; _guestRoom=code; stopPrev(); ov.remove(); try{ await sfuInit(); }catch(_){} + // #4a: remember the pre-join choices; they're applied in the meeting-joined handler — i.e. AFTER the + // host admits the guest and media is connected — not on a blind timer that fires while still in the lobby. + _pendingJoinMic=joinMic; _pendingJoinCam=joinCam; switchTab('meeting'); await enterMeeting(code); - // Apply their pre-join choices once the call is up. - setTimeout(()=>{ try{ if(joinMic && !meetMic) toggleMic(); if(joinCam && !meetCam) toggleCam(); }catch(_){} }, 900); }; go.onclick=join; inp.addEventListener('keydown',e=>{ if(e.key==='Enter'){ e.preventDefault(); join(); } }); diff --git a/server/signaling.js b/server/signaling.js index 9bfd044..40a5e59 100644 --- a/server/signaling.js +++ b/server/signaling.js @@ -406,8 +406,11 @@ async function leaveMeeting(ws) { if (!peers) { if (leaverId) CHAT.broadcastPresence(leaverId); return; } try { await require('./calls').finalizeTranscript(room, ws._meetingUserId); } catch (_) {} // save THIS user's transcript peers.delete(pid); - // 1:1 call: when either party leaves, end it for everyone (a DM call has no "remaining" call). - if (roomToDmCall.has(room)) { + // 1:1 call: end it for everyone ONLY when fewer than two people would remain. A DM call that had + // extra people ADDED (via "Add people") is effectively a group now — one participant closing their + // app must NOT hang up the call for the rest (#11). We only tear the whole thing down when ≤1 person + // is left (nobody to talk to). With 2+ remaining we fall through to the normal peer-left path below. + if (roomToDmCall.has(room) && peers.size < 2) { const others = [...peers.values()].map((p) => p.ws && p.ws._meetingUserId).filter(Boolean); for (const [, p] of peers) { if (p.ws.readyState === 1) { try { p.ws.send(JSON.stringify({ type: 'meeting-ended' })); } catch (_) {} p.ws._meetingRoom = null; } } await persistCallHistory(room); // #7: log the call BEFORE endCallByRoom clears roomToDmCall/roomToGroupCall