diff --git a/server/calls.js b/server/calls.js index 2a68ade..bdd6785 100644 --- a/server/calls.js +++ b/server/calls.js @@ -133,6 +133,10 @@ function endCallByRoom(room) { endGroupCallByRoom(room); endDmCallByRoom(room); function declineDmCall(room, byUser) { const key = roomToDmCall.get(room); if (!key) return { ok: false }; const call = dmCalls.get(key); if (!call) return { ok: false }; + // #8: if this user has ALREADY accepted on another device (they're in the room), this is just the + // ringing invite on a second device — dismiss it silently, do NOT tear down the active call. + const inRoom = meetingRooms.get(room); + if (inRoom) { for (const [, p] of inRoom) { if (p.ws && p.ws._meetingUserId === byUser.id) return { ok: true, alreadyJoined: true }; } } const callerId = call.users.find((id) => id !== byUser.id) || call.startedBy; try { const mid = A.id(); diff --git a/server/public/home.html b/server/public/home.html index 29c47c8..efd6fbc 100644 --- a/server/public/home.html +++ b/server/public/home.html @@ -349,6 +349,9 @@ .meet{display:flex;flex-direction:column;width:100%;height:100%;} .meet-grid{flex:1;min-height:0;display:grid;gap:.6rem;padding:.8rem;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));align-content:center;overflow:hidden;background:#0b1220;} /* #16: center tiles + no stray scrollbar on 1:1/small calls */ .meet-tile{position:relative;background:#0b1220;border-radius:12px;overflow:hidden;aspect-ratio:4/3;border:1px solid #1e293b;transition:box-shadow .12s,border-color .12s;} + .meet-tile.waiting{border-color:#334155;} + .meet-tile.waiting .ringing{position:absolute;left:0;right:0;bottom:0;display:flex;align-items:center;justify-content:center;gap:.3rem;font-size:.8rem;color:#cbd5e1;background:rgba(0,0,0,.35);padding:.35rem;animation:ringPulse 1.4s ease-in-out infinite;} + @keyframes ringPulse{0%,100%{opacity:.6;}50%{opacity:1;}} .meet-tile.speaking{border-color:#22c55e;box-shadow:0 0 0 2px #22c55e, 0 0 14px rgba(34,197,94,.5);} .meet-tile .meet-screen{position:absolute;right:.5rem;top:.5rem;display:inline-flex;align-items:center;gap:.25rem;background:rgba(37,99,235,.92);color:#fff;font-size:.66rem;font-weight:700;padding:.14rem .42rem;border-radius:6px;} .meet-tile.sharing{grid-column:span 2;border-color:#2563eb;} @@ -823,7 +826,7 @@
- @@ -1717,7 +1720,7 @@ function onGroupCall(d){ function dismissCallInvite(room){ if(!room) return; const el=document.getElementById('ci-'+room); if(el){ try{ el.remove(); }catch(_){} stopRing(); } } // 1:1 call: start/join from the DM header; live state updates the button + shows an incoming invite. async function startOrJoinDmCall(otherId){ - try{ const r=await postJSON('/api/calls/dm/start',{ to:otherId }); if(r&&r.room){ meetReturn={kind:'dm',id:otherId}; switchTab('meeting'); enterMeeting(r.room); startRingback(); /* #17: ring until they answer */ } } + try{ const r=await postJSON('/api/calls/dm/start',{ to:otherId }); if(r&&r.room){ const _r=rowFor('dm',otherId); _dmCallWaiting={ name:(_r&&_r.name)||'Contact', avatar:(_r&&_r.avatar)||null }; meetReturn={kind:'dm',id:otherId}; switchTab('meeting'); enterMeeting(r.room); startRingback(); /* #17: ring until they answer */ } } catch(e){ toast(e.message||'Could not start the call'); } } // Live presence: a contact came online/offline or entered/left a call — update their dot + the @@ -2719,8 +2722,12 @@ function sfuRebuild(pid){ const vid=p.screen||p.cam; if(vid) st.addTrack(vid); if(p.audio) st.addTrack(p.audio); addTile(pid, st, meetNames.get(pid)||'Guest', false); setTileScreen(pid, !!p.screen); meetWatchStream(pid, st); } -function sfuAttach(pub, track, participant){ - const pid=peerIdForUid(participant.identity); if(!pid) return; +function sfuAttach(pub, track, participant, _try){ + const pid=peerIdForUid(participant.identity); + if(!pid){ // #9: uid→peerId map (from the mesh join) may lag the LiveKit track — retry a few times + if((_try||0)<10){ setTimeout(()=>sfuAttach(pub,track,participant,(_try||0)+1), 400); } + return; + } const p=SFU.peers.get(pid)||{}; SFU.peers.set(pid,p); const mt=track.mediaStreamTrack; if(track.kind==='audio') p.audio=mt; else if(pub.source===SFU.lib.Track.Source.ScreenShare){ p.screen=mt; meetSharers.add(pid); } @@ -2984,6 +2991,16 @@ function addTile(id, stream, label, muted){ } function setTileMute(id, muted){ meetMuted.set(id, !!muted); const t=document.getElementById('meet-tile-'+id); if(t){ const b=t.querySelector('.meet-mute'); if(b) b.style.display=muted?'grid':'none'; if(muted) t.classList.remove('speaking'); } } function removeTile(id){ const t=document.getElementById('meet-tile-'+id); if(t) t.remove(); meetMuted.delete(id); meetCamOff.delete(id); meetUnwatch(id); } +// #7: on an outgoing 1:1 call, show the person you're calling (DP + name + "Ringing…") so it's clear +// who the call is to, instead of only your own tile. Removed as soon as they answer. +let _dmCallWaiting=null; +function addWaitingTile(name, avatar){ + const grid=document.getElementById('meetGrid'); if(!grid || document.getElementById('meet-tile-__waiting')) return; + const tile=document.createElement('div'); tile.className='meet-tile novid waiting'; tile.id='meet-tile-__waiting'; + tile.innerHTML='