feat: start group call on first answer, show pending peers
The call now starts immediately when the first peer answers instead of waiting for all participants. Remaining peers are tracked as pending and shown in the UI with a "Waiting for others to join..." indicator. When a pending peer answers, they move to the connected set. If a pending peer rejects or hangs up, they're silently removed. Changes: - Add pendingPeerPubKeys to Connecting and Connected states - Add allPeerPubKeys helper on Connected for UI rendering - Split peers into connected/pending on first answer in onCallAnswered - Handle subsequent answers in Connecting and Connected states - Carry pending peers through Connecting -> Connected transition - Show all peers (connected + pending) in call UI avatars - Display "Waiting for others to join..." when pending peers exist https://claude.ai/code/session_01LR8NmFGdMoDTVcfKjL7HWR
This commit is contained in:
@@ -427,13 +427,13 @@ private fun ConnectedCallUI(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
GroupCallPictures(
|
||||
peerPubKeys = state.peerPubKeys,
|
||||
peerPubKeys = state.allPeerPubKeys,
|
||||
size = 120.dp,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
GroupCallNames(
|
||||
peerPubKeys = state.peerPubKeys,
|
||||
peerPubKeys = state.allPeerPubKeys,
|
||||
accountViewModel = accountViewModel,
|
||||
textColor = Color.White,
|
||||
)
|
||||
@@ -443,6 +443,14 @@ private fun ConnectedCallUI(
|
||||
color = Color.White.copy(alpha = 0.7f),
|
||||
fontSize = 16.sp,
|
||||
)
|
||||
if (state.pendingPeerPubKeys.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringRes(R.string.call_waiting_for_others),
|
||||
color = Color.White.copy(alpha = 0.5f),
|
||||
fontSize = 13.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Timer overlay
|
||||
@@ -648,7 +656,7 @@ private fun PipConnectedCallUI(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
GroupCallPictures(
|
||||
peerPubKeys = state.peerPubKeys,
|
||||
peerPubKeys = state.allPeerPubKeys,
|
||||
size = 48.dp,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
|
||||
@@ -796,6 +796,7 @@
|
||||
<string name="call_bluetooth">Bluetooth</string>
|
||||
<string name="call_voice">Voice call</string>
|
||||
<string name="call_video">Video call</string>
|
||||
<string name="call_waiting_for_others">Waiting for others to join\u2026</string>
|
||||
<string name="call_failed_start">Failed to start call</string>
|
||||
<string name="call_failed_accept">Failed to accept call</string>
|
||||
<string name="call_failed_session">Failed to create call session</string>
|
||||
|
||||
+71
-15
@@ -155,15 +155,37 @@ class CallManager(
|
||||
fun onCallAnswered(event: CallAnswerEvent) {
|
||||
val current = _state.value
|
||||
val callId = event.callId()
|
||||
val answeringPeer = event.pubKey
|
||||
|
||||
when (current) {
|
||||
is CallState.Offering -> {
|
||||
if (callId != current.callId) return
|
||||
_state.value = CallState.Connecting(current.callId, current.peerPubKeys, current.callType)
|
||||
// First answer: start the call immediately. Remaining peers stay pending.
|
||||
val pending = current.peerPubKeys - answeringPeer
|
||||
_state.value =
|
||||
CallState.Connecting(
|
||||
current.callId,
|
||||
setOf(answeringPeer),
|
||||
current.callType,
|
||||
pendingPeerPubKeys = pending,
|
||||
)
|
||||
cancelTimeout()
|
||||
onAnswerReceived?.invoke(event)
|
||||
}
|
||||
|
||||
is CallState.Connecting -> {
|
||||
// Another peer answered while we're still connecting with the first
|
||||
if (callId != current.callId) return
|
||||
if (answeringPeer in current.pendingPeerPubKeys) {
|
||||
_state.value =
|
||||
current.copy(
|
||||
peerPubKeys = current.peerPubKeys + answeringPeer,
|
||||
pendingPeerPubKeys = current.pendingPeerPubKeys - answeringPeer,
|
||||
)
|
||||
}
|
||||
// TODO: establish additional WebRTC peer connection for this peer
|
||||
}
|
||||
|
||||
is CallState.IncomingCall -> {
|
||||
// Another device of this user answered the call — stop ringing.
|
||||
if (callId != current.callId) return
|
||||
@@ -171,9 +193,19 @@ class CallManager(
|
||||
}
|
||||
|
||||
is CallState.Connected -> {
|
||||
// Renegotiation answer (e.g., peer accepted our video upgrade offer)
|
||||
if (callId != current.callId) return
|
||||
onAnswerReceived?.invoke(event)
|
||||
if (answeringPeer in current.pendingPeerPubKeys) {
|
||||
// A pending peer just joined the group call
|
||||
_state.value =
|
||||
current.copy(
|
||||
peerPubKeys = current.peerPubKeys + answeringPeer,
|
||||
pendingPeerPubKeys = current.pendingPeerPubKeys - answeringPeer,
|
||||
)
|
||||
// TODO: establish additional WebRTC peer connection for this peer
|
||||
} else {
|
||||
// Renegotiation answer (e.g., peer accepted our video upgrade offer)
|
||||
onAnswerReceived?.invoke(event)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
@@ -198,6 +230,20 @@ class CallManager(
|
||||
}
|
||||
}
|
||||
|
||||
is CallState.Connecting -> {
|
||||
// A pending peer rejected while we're already connecting with another
|
||||
if (callId != current.callId) return
|
||||
_state.value =
|
||||
current.copy(pendingPeerPubKeys = current.pendingPeerPubKeys - rejectingPeer)
|
||||
}
|
||||
|
||||
is CallState.Connected -> {
|
||||
// A pending peer rejected while we're already in the call
|
||||
if (callId != current.callId) return
|
||||
_state.value =
|
||||
current.copy(pendingPeerPubKeys = current.pendingPeerPubKeys - rejectingPeer)
|
||||
}
|
||||
|
||||
is CallState.IncomingCall -> {
|
||||
// Another device of this user rejected the call — stop ringing.
|
||||
if (callId != current.callId) return
|
||||
@@ -251,6 +297,7 @@ class CallManager(
|
||||
peerPubKeys = current.peerPubKeys,
|
||||
callType = current.callType,
|
||||
startedAtEpoch = TimeUtils.now(),
|
||||
pendingPeerPubKeys = current.pendingPeerPubKeys,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -296,21 +343,31 @@ class CallManager(
|
||||
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)
|
||||
val connectedRemaining = current.peerPubKeys - leavingPeer
|
||||
val pendingRemaining = current.pendingPeerPubKeys - leavingPeer
|
||||
if (connectedRemaining.isEmpty() && pendingRemaining.isEmpty()) {
|
||||
transitionToEnded(callId, current.allPeerPubKeys, EndReason.PEER_HANGUP)
|
||||
} else {
|
||||
_state.value = current.copy(peerPubKeys = remaining)
|
||||
_state.value =
|
||||
current.copy(
|
||||
peerPubKeys = connectedRemaining,
|
||||
pendingPeerPubKeys = pendingRemaining,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is CallState.Connecting -> {
|
||||
if (callId != current.callId) return
|
||||
val remaining = current.peerPubKeys - leavingPeer
|
||||
if (remaining.isEmpty()) {
|
||||
transitionToEnded(callId, current.peerPubKeys, EndReason.PEER_HANGUP)
|
||||
val connectedRemaining = current.peerPubKeys - leavingPeer
|
||||
val pendingRemaining = current.pendingPeerPubKeys - leavingPeer
|
||||
if (connectedRemaining.isEmpty() && pendingRemaining.isEmpty()) {
|
||||
transitionToEnded(callId, current.peerPubKeys + current.pendingPeerPubKeys, EndReason.PEER_HANGUP)
|
||||
} else {
|
||||
_state.value = current.copy(peerPubKeys = remaining)
|
||||
_state.value =
|
||||
current.copy(
|
||||
peerPubKeys = connectedRemaining,
|
||||
pendingPeerPubKeys = pendingRemaining,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,7 +383,6 @@ class CallManager(
|
||||
|
||||
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 {
|
||||
@@ -376,13 +432,13 @@ class CallManager(
|
||||
/** Returns the first peer pubkey (for P2P calls) or null. */
|
||||
fun currentPeerPubKey(): HexKey? = currentPeerPubKeys()?.firstOrNull()
|
||||
|
||||
/** Returns all peer pubkeys for the current call. */
|
||||
/** Returns all peer pubkeys for the current call (connected + pending). */
|
||||
fun currentPeerPubKeys(): Set<HexKey>? =
|
||||
when (val s = _state.value) {
|
||||
is CallState.Offering -> s.peerPubKeys
|
||||
is CallState.IncomingCall -> s.peerPubKeys()
|
||||
is CallState.Connecting -> s.peerPubKeys
|
||||
is CallState.Connected -> s.peerPubKeys
|
||||
is CallState.Connecting -> s.peerPubKeys + s.pendingPeerPubKeys
|
||||
is CallState.Connected -> s.allPeerPubKeys
|
||||
else -> null
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ sealed interface CallState {
|
||||
val callId: String,
|
||||
val peerPubKeys: Set<HexKey>,
|
||||
val callType: CallType,
|
||||
val pendingPeerPubKeys: Set<HexKey> = emptySet(),
|
||||
) : CallState
|
||||
|
||||
data class Connected(
|
||||
@@ -53,7 +54,11 @@ sealed interface CallState {
|
||||
val peerPubKeys: Set<HexKey>,
|
||||
val callType: CallType,
|
||||
val startedAtEpoch: Long,
|
||||
) : CallState
|
||||
val pendingPeerPubKeys: Set<HexKey> = emptySet(),
|
||||
) : CallState {
|
||||
/** All peers: connected + still pending. */
|
||||
val allPeerPubKeys: Set<HexKey> get() = peerPubKeys + pendingPeerPubKeys
|
||||
}
|
||||
|
||||
data class Ended(
|
||||
val callId: String,
|
||||
|
||||
Reference in New Issue
Block a user