From 3962f75583d932cb20ce6d58ed581990a25e5ed5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Apr 2026 00:29:24 +0000 Subject: [PATCH] feat: keep group call alive when a peer leaves until only 2 remain When a peer hangs up or rejects in a group call, remove them from the participant set instead of ending the entire call. The call only ends when no peers remain (all others left). Similarly, if a peer rejects during the Offering phase of a group call, remove them but continue ringing remaining peers. For incoming group calls, if the original caller hangs up the call ends immediately. If another group member leaves, the call continues as long as at least 2 members (including self) remain. https://claude.ai/code/session_01LR8NmFGdMoDTVcfKjL7HWR --- .../amethyst/commons/call/CallManager.kt | 70 +++++++++++++++---- 1 file changed, 58 insertions(+), 12 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 1d879d65d..b025cdc94 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 @@ -185,11 +185,17 @@ class CallManager( fun onCallRejected(event: CallRejectEvent) { val current = _state.value val callId = event.callId() + val rejectingPeer = event.pubKey when (current) { is CallState.Offering -> { if (callId != current.callId) return - transitionToEnded(current.callId, current.peerPubKeys, EndReason.PEER_REJECTED) + val remaining = current.peerPubKeys - rejectingPeer + if (remaining.isEmpty()) { + transitionToEnded(current.callId, current.peerPubKeys, EndReason.PEER_REJECTED) + } else { + _state.value = current.copy(peerPubKeys = remaining) + } } is CallState.IncomingCall -> { @@ -285,18 +291,58 @@ class CallManager( fun onPeerHangup(event: CallHangupEvent) { val current = _state.value val callId = event.callId() ?: return - val currentCallId = - when (current) { - is CallState.Offering -> current.callId - is CallState.Connecting -> current.callId - is CallState.Connected -> current.callId - is CallState.IncomingCall -> current.callId - else -> return - } - if (callId != currentCallId) return + val leavingPeer = event.pubKey - val peerPubKeys = currentPeerPubKeys() ?: return - transitionToEnded(callId, peerPubKeys, EndReason.PEER_HANGUP) + when (current) { + is CallState.Connected -> { + if (callId != current.callId) return + val remaining = current.peerPubKeys - leavingPeer + if (remaining.isEmpty()) { + transitionToEnded(callId, current.peerPubKeys, EndReason.PEER_HANGUP) + } else { + _state.value = current.copy(peerPubKeys = remaining) + } + } + + is CallState.Connecting -> { + if (callId != current.callId) return + val remaining = current.peerPubKeys - leavingPeer + if (remaining.isEmpty()) { + transitionToEnded(callId, current.peerPubKeys, EndReason.PEER_HANGUP) + } else { + _state.value = current.copy(peerPubKeys = remaining) + } + } + + is CallState.Offering -> { + if (callId != current.callId) return + val remaining = current.peerPubKeys - leavingPeer + if (remaining.isEmpty()) { + transitionToEnded(callId, current.peerPubKeys, EndReason.PEER_HANGUP) + } else { + _state.value = current.copy(peerPubKeys = remaining) + } + } + + is CallState.IncomingCall -> { + if (callId != current.callId) return + // If the caller hung up, end the incoming call + if (leavingPeer == current.callerPubKey) { + transitionToEnded(callId, current.groupMembers, EndReason.PEER_HANGUP) + } else { + val remaining = current.groupMembers - leavingPeer + if (remaining.size <= 1) { + transitionToEnded(callId, current.groupMembers, EndReason.PEER_HANGUP) + } else { + _state.value = current.copy(groupMembers = remaining) + } + } + } + + else -> { + return + } + } } fun onSignalingEvent(event: Event) {