call: stop the ring on a user's other devices when one answers (answered-elsewhere)
A user signed in on two devices (iPhone + Android) rang on both. When they
answered on Android, the iPhone kept ringing: markDmAnswered only emitted a
WS 'call-taken', which never reaches a backgrounded/killed device ringing on
CallKit (no live socket).
Now markDmAnswered also pushes a native cancel to the answerer's OTHER
devices. The device that actually answered is skipped via exceptToken (its own
call-push token) — critical on iOS, where a cancel would end the just-answered
CallKit call; Android's cancel branch never tears down a live call anyway.
- push.sendCallCancel(userId, uuid, { exceptToken, reason }) — skips the
answering device's token; existing 2-arg callers unchanged.
- calls.markDmAnswered(room, userId, exceptToken) — fires the cancel.
- POST /api/calls/answered now takes deviceToken.
- home.html: the native answerCall handler POSTs /api/calls/answered with this
device's own token (ios-voip or fcm), so native/LiveKit calls (which bypass
the mesh) are marked answered and siblings stop ringing.
Server + web only — no native build needed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
aa794102c3
commit
08eff043ec
4 files changed
+36
-11
No files matched your search
+17
-6
@@ -203,23 +203,34 @@ async function sendCallNotification(userId, data) {
|
||||
return 'push';
|
||||
}
|
||||
|
||||
// Tell a user's device(s) to STOP ringing a call (caller hung up / declined / ring timed out). Sent as a
|
||||
// VoIP push so it reaches a killed app that has no WebSocket — the CallKit plugin ends the reported call.
|
||||
// No-op for non-VoIP devices (their ring is a normal notification that just goes away).
|
||||
async function sendCallCancel(userId, callUUID) {
|
||||
// Tell a user's device(s) to STOP ringing a call (caller hung up / declined / ring timed out / answered on
|
||||
// another device). Sent as a VoIP push (iOS) / high-priority data message (Android) so it reaches a killed app
|
||||
// that has no WebSocket — the native plugin ends/dismisses the reported call. No-op for non-native devices
|
||||
// (their ring is a normal notification that just goes away).
|
||||
// opts.exceptToken — skip this exact device token. Used for "answered elsewhere": the device that actually
|
||||
// answered must NOT be cancelled (on iOS a cancel ends the just-answered CallKit call), only its SIBLINGS.
|
||||
// opts.reason — optional tag carried in the payload ('answered' etc.), for logs / a future answered-elsewhere
|
||||
// call-history entry. Native plugins ignore unknown fields, so it's safe to always include.
|
||||
async function sendCallCancel(userId, callUUID, opts) {
|
||||
if (!callUUID) return;
|
||||
const exceptToken = opts && opts.exceptToken;
|
||||
const reason = opts && opts.reason;
|
||||
let toks = [];
|
||||
try { toks = await R.deviceTokens.byUser(userId); } catch (_) { toks = []; }
|
||||
// iOS: cancel the CallKit ring via a VoIP push.
|
||||
if (apnsCfg && process.env.CALLKIT_ENABLED === '1') {
|
||||
for (const t of toks.filter((t) => t.platform === 'ios-voip')) {
|
||||
try { const r = await sendApnsVoip(t.token, { type: 'cancel', callUUID }); if (r && r.dead) { try { await R.deviceTokens.removeByToken(t.token); } catch (_) {} } } catch (_) {}
|
||||
if (exceptToken && t.token === exceptToken) continue; // never cancel the device that answered
|
||||
const payload = reason ? { type: 'cancel', callUUID, reason } : { type: 'cancel', callUUID };
|
||||
try { const r = await sendApnsVoip(t.token, payload); if (r && r.dead) { try { await R.deviceTokens.removeByToken(t.token); } catch (_) {} } } catch (_) {}
|
||||
}
|
||||
}
|
||||
// Android: cancel the native ring via a high-priority data message (BzCallMessagingService dismisses it).
|
||||
if (fcmSA && process.env.ANDROID_CALLS === '1') {
|
||||
for (const t of toks.filter((t) => t.platform === 'android')) {
|
||||
try { const r = await sendFcmData(t.token, { bzcall: '1', type: 'cancel', callUUID }); if (r && r.dead) { try { await R.deviceTokens.removeByToken(t.token); } catch (_) {} } } catch (_) {}
|
||||
if (exceptToken && t.token === exceptToken) continue; // never cancel the device that answered
|
||||
const data = reason ? { bzcall: '1', type: 'cancel', callUUID, reason } : { bzcall: '1', type: 'cancel', callUUID };
|
||||
try { const r = await sendFcmData(t.token, data); if (r && r.dead) { try { await R.deviceTokens.removeByToken(t.token); } catch (_) {} } } catch (_) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user