From 500928f1b3e9f2f3a6ce12c3c6b98d45ba43baa0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Apr 2026 18:09:33 +0000 Subject: [PATCH] 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 --- .../amethyst/commons/call/CallManager.kt | 36 +++++++++++++++++-- .../amethyst/commons/call/CallState.kt | 1 + 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/call/CallManager.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/call/CallManager.kt index 581969506..c77ba16b9 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/call/CallManager.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/call/CallManager.kt @@ -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) { diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/call/CallState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/call/CallState.kt index 79334a527..65169aa4c 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/call/CallState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/call/CallState.kt @@ -68,4 +68,5 @@ enum class EndReason { ERROR, PEER_HANGUP, PEER_REJECTED, + ANSWERED_ELSEWHERE, }