Native calls: fix answered-call drop, multi-device ring, add in-app call screen
Root cause of "answered but the call disconnects": native calls carry media
over LiveKit and bypass the mesh, so the server only learned "answered" from a
WebView POST. On a cold/locked answer the app is still launching and that event
was lost, so the 40s unanswered timer fired and cancelled the live call. Fix:
- Plugin: notifyListeners("answerCall", retainUntilConsumed:true) so a killed/
locked pickup isn't lost before the WebView JS attaches.
- Server markDmAnswered: emit call-taken to the callee's OTHER devices (stop the
ring; no teardown) and call-answered to the caller (flip UI to connected).
- Server declineDmCall: ignore a decline once the call is answered, so dismissing
a stale ring on a second device can't kill the live call.
Also adds a UI-only in-app call screen for native calls (caller + callee):
avatar, name, live timer, mute (-> plugin), end (-> CallKit). Native media has
no meeting window of its own; this covers "no meeting window / can't unmute".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+14
-1
@@ -162,7 +162,16 @@ async function endDmCallByRoom(room, silent) {
|
||||
function markDmAnswered(room, userId) {
|
||||
const key = roomToDmCall.get(room); if (!key) return;
|
||||
const call = dmCalls.get(key); if (!call) return;
|
||||
if (userId && userId !== call.startedBy && !call.answered) { call.answered = true; call.answeredAt = now(); if (call.ringTimer) { try { clearTimeout(call.ringTimer); } catch (_) {} call.ringTimer = null; } }
|
||||
if (userId && userId !== call.startedBy && !call.answered) {
|
||||
call.answered = true; call.answeredAt = now();
|
||||
if (call.ringTimer) { try { clearTimeout(call.ringTimer); } catch (_) {} call.ringTimer = null; }
|
||||
// Native calls carry media over LiveKit, not our mesh — so the callee's OTHER devices never learn the
|
||||
// call was picked up here and keep ringing forever (and dismissing that stale ring as a "decline" would
|
||||
// tear down THIS live call). Tell them it was taken (dismiss the ring, no teardown), and flip the
|
||||
// caller's UI from "ringing" to "connected".
|
||||
try { CHAT.pushToUser(userId, { type: 'call-taken', room, uuid: call.uuid }); } catch (_) {}
|
||||
try { CHAT.pushToUser(call.startedBy, { type: 'call-answered', room, uuid: call.uuid, by: userId }); } catch (_) {}
|
||||
}
|
||||
}
|
||||
// When a user's chat socket (re)connects, re-send any call they're currently being rung into. The
|
||||
// original dm-call / group-call events fire ONCE at call start, so an app that was closed then misses
|
||||
@@ -195,6 +204,10 @@ async function endCallByRoom(room) { await endGroupCallByRoom(room); await endDm
|
||||
async function declineDmCall(room, byUser) {
|
||||
const key = roomToDmCall.get(room); if (!key) return { ok: false };
|
||||
const call = dmCalls.get(key); if (!call) return { ok: false };
|
||||
// Already ANSWERED (e.g. a native CallKit pickup on this user's other device, which bypasses our mesh so
|
||||
// the check below can't see it): a "decline" here is just the stale ring on a second device — dismiss it,
|
||||
// do NOT tear down the live call.
|
||||
if (call.answered && byUser.id !== call.startedBy) return { ok: true, alreadyAnswered: true };
|
||||
// #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);
|
||||
|
||||
Reference in New Issue
Block a user