From ea65dd76e7b77ec2b91aff76854b1146372911c6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Apr 2026 02:49:37 +0000 Subject: [PATCH] fix: correct group call hangup signaling and connection resilience - Include pending peers in hangup notification so ringing users get immediate feedback instead of waiting for the 60s timeout - Sign individual hangup events per peer with correct p-tag instead of reusing a single event with the first peer's tag - Stop treating ICE DISCONNECTED as terminal; only hang up on FAILED since DISCONNECTED is often transient (network switch, packet loss) - Send "busy" reject when receiving a call while already in one https://claude.ai/code/session_01HpowdWMK77pA35ABn9XWpD --- .../amethyst/service/call/WebRtcCallSession.kt | 5 ++++- .../amethyst/commons/call/CallManager.kt | 14 +++++++++++--- .../quartz/nipACWebRtcCalls/WebRtcCallFactory.kt | 10 +++++++--- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/WebRtcCallSession.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/WebRtcCallSession.kt index 0d8b50e3c..e70549dfa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/WebRtcCallSession.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/WebRtcCallSession.kt @@ -112,7 +112,10 @@ class WebRtcCallSession( } PeerConnection.IceConnectionState.DISCONNECTED -> { - onDisconnected() + // DISCONNECTED is often transient (network switch, brief + // packet loss). WebRTC will transition to FAILED if + // recovery is impossible, so we only act on FAILED above. + Log.d(TAG) { "ICE disconnected (transient, waiting for recovery or FAILED)" } } else -> {} 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 724cab6a7..6e1a7969f 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 @@ -110,7 +110,15 @@ class CallManager( if (!isFollowing(callerPubKey)) return - if (_state.value !is CallState.Idle) return + if (_state.value !is CallState.Idle) { + // Already in a call — send a "busy" reject so the caller gets + // immediate feedback instead of waiting for the 60s timeout. + scope.launch { + val result = factory.createReject(callerPubKey, callId, "busy", signer = signer) + publishEvent(result.wrap) + } + return + } val groupMembers = event.groupMembers() @@ -341,12 +349,12 @@ class CallManager( } is CallState.Connecting -> { - peerPubKeys = current.peerPubKeys + peerPubKeys = current.peerPubKeys + current.pendingPeerPubKeys callId = current.callId } is CallState.Connected -> { - peerPubKeys = current.peerPubKeys + peerPubKeys = current.allPeerPubKeys callId = current.callId } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/WebRtcCallFactory.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/WebRtcCallFactory.kt index c517eb723..845d397ae 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/WebRtcCallFactory.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/WebRtcCallFactory.kt @@ -157,13 +157,17 @@ class WebRtcCallFactory { reason: String = "", signer: NostrSigner, ): GroupResult { - val template = CallHangupEvent.build(peerPubKeys.first(), callId, reason) - val signed = signer.sign(template) + // Each peer gets its own signed hangup with the correct `p` tag + // targeting that specific recipient. + var firstSigned: Event? = null val wraps = peerPubKeys.map { pubKey -> + val template = CallHangupEvent.build(pubKey, callId, reason) + val signed = signer.sign(template) + if (firstSigned == null) firstSigned = signed GiftWrapEvent.create(event = signed, recipientPubKey = pubKey, expirationDelta = WRAP_EXPIRATION_SECONDS) } - return GroupResult(signed, wraps) + return GroupResult(firstSigned!!, wraps) } /**