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
This commit is contained in:
Claude
2026-04-03 00:29:24 +00:00
parent 4e2143641f
commit 3962f75583
@@ -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) {