fix: stop ringing on other devices when call is answered or rejected

When a user is logged in on multiple devices, all devices ring on an
incoming call. Previously, accepting or rejecting on one device left the
others still ringing.

Now, acceptCall() and rejectCall() also publish a self-addressed
gift-wrapped event (to the user's own pubkey) so other devices receive
it and transition out of the IncomingCall state. Added ANSWERED_ELSEWHERE
end reason and handling in onCallAnswered/onCallRejected for IncomingCall
state.

https://claude.ai/code/session_01X9juzyPYWqxRMCgDWEw8R1
This commit is contained in:
Claude
2026-04-02 18:09:33 +00:00
parent 13de5d5eee
commit 500928f1b3
2 changed files with 34 additions and 3 deletions
@@ -108,6 +108,12 @@ class CallManager(
_state.value = CallState.Connecting(current.callId, current.callerPubKey, current.callType)
cancelTimeout()
publishEvent(result.wrap)
// Notify other devices of this user that the call was answered here.
// This gift-wraps an answer event to our own pubkey so other logged-in
// devices see it and stop ringing.
val selfNotify = factory.createCallAnswer(sdpAnswer, signer.pubKey, current.callId, signer)
publishEvent(selfNotify.wrap)
}
suspend fun rejectCall() {
@@ -117,6 +123,10 @@ class CallManager(
val result = factory.createReject(current.callerPubKey, current.callId, signer = signer)
transitionToEnded(current.callId, current.callerPubKey, EndReason.REJECTED)
publishEvent(result.wrap)
// Notify other devices of this user that the call was rejected here.
val selfNotify = factory.createReject(signer.pubKey, current.callId, signer = signer)
publishEvent(selfNotify.wrap)
}
fun onCallAnswered(event: CallAnswerEvent) {
@@ -131,6 +141,12 @@ class CallManager(
onAnswerReceived?.invoke(event)
}
is CallState.IncomingCall -> {
// Another device of this user answered the call — stop ringing.
if (callId != current.callId) return
transitionToEnded(current.callId, current.callerPubKey, EndReason.ANSWERED_ELSEWHERE)
}
is CallState.Connected -> {
// Renegotiation answer (e.g., peer accepted our video upgrade offer)
if (callId != current.callId) return
@@ -145,10 +161,24 @@ class CallManager(
fun onCallRejected(event: CallRejectEvent) {
val current = _state.value
if (current !is CallState.Offering) return
if (event.callId() != current.callId) return
val callId = event.callId()
transitionToEnded(current.callId, current.peerPubKey, EndReason.PEER_REJECTED)
when (current) {
is CallState.Offering -> {
if (callId != current.callId) return
transitionToEnded(current.callId, current.peerPubKey, EndReason.PEER_REJECTED)
}
is CallState.IncomingCall -> {
// Another device of this user rejected the call — stop ringing.
if (callId != current.callId) return
transitionToEnded(current.callId, current.callerPubKey, EndReason.REJECTED)
}
else -> {
return
}
}
}
fun onIceCandidate(event: CallIceCandidateEvent) {
@@ -68,4 +68,5 @@ enum class EndReason {
ERROR,
PEER_HANGUP,
PEER_REJECTED,
ANSWERED_ELSEWHERE,
}