#15: adding a 3rd person to a 1:1 call promotes it to a persistent group call

When someone is added to a 1:1 (DM) call, promoteDmToGroup() now creates a
real group conversation (named after the participants) and migrates the live
call from dmCalls -> groupCalls, keeping the same room/uuid/history so media
and the transcript continue uninterrupted. Two wins:
 - the call survives anyone leaving (group calls only end when the room empties)
 - an added person who drops can rejoin from the group's active-call banner
   (they're now a member, so replayActiveCalls / group-call resurface it)

/api/calls/invite promotes on a DM call and lets the group-call broadcast ring
the invitees in; it only sends the plain call-invite when NOT promoted. Guarded
so inviting an existing pair-member (memberIds < 3) stays a 1:1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 22:13:41 +05:30
parent 100a092ff9
commit a0f9ab10f2
2 changed files with 44 additions and 3 deletions
+7 -2
View File
@@ -963,8 +963,13 @@ route('POST', '/api/calls/invite', async (req, res) => {
const { room, userIds } = await readBody(req);
if (!room || !meetingRooms.has(String(room))) return json(res, 404, { error: 'call not found' });
const ids = await asyncFilter((Array.isArray(userIds) ? userIds : []), async (x) => typeof x === 'string' && x !== u.id && await R.users.inTenant(x, u.team_id));
for (const id of ids) { try { CHAT.pushToUser(id, { type: 'call-invite', room: String(room), byName: (u.name || u.email) }); } catch (_) {} }
json(res, 200, { ok: true, invited: ids.length });
// #15: adding people to a 1:1 call promotes it to a persistent GROUP call (survives leaves + lets an added
// person rejoin after dropping). When that happens promoteDmToGroup's own group-call broadcast already rings
// the invitees in, so we only send the plain call-invite for a call that WASN'T promoted (group/ad-hoc).
let groupId = null;
try { groupId = await CALLS.promoteDmToGroup(String(room), u, ids); } catch (_) {}
if (!groupId) { for (const id of ids) { try { CHAT.pushToUser(id, { type: 'call-invite', room: String(room), byName: (u.name || u.email) }); } catch (_) {} } }
json(res, 200, { ok: true, invited: ids.length, group: groupId || undefined });
});
// Does this deployment use the LiveKit SFU for meeting media? The client asks on load; if sfu is