From fc448ecb59f095c689698b60a28c72ded86dc6cd Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Tue, 12 May 2026 10:47:28 -0300 Subject: [PATCH] Fix call screen crash when participant count drops during speaker view. --- .../webrtc/v2/CallParticipantsPager.kt | 93 ++++++++----------- .../components/webrtc/v2/CallScreen.kt | 3 +- .../webrtc/v2/CallScreenController.kt | 4 +- .../webrtc/v2/ComposeCallScreenMediator.kt | 1 + 4 files changed, 46 insertions(+), 55 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallParticipantsPager.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallParticipantsPager.kt index 05d39cc1e5..0798594e3d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallParticipantsPager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallParticipantsPager.kt @@ -15,7 +15,6 @@ import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.getValue -import androidx.compose.runtime.movableContentOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState @@ -52,67 +51,55 @@ fun CallParticipantsPager( callParticipantsPagerState.callParticipants.firstOrNull()?.videoSink ) - // Use movableContentOf to preserve CallGrid state when switching between - // single participant (no pager) and multiple participants (with pager) - val callGridContent = remember { - movableContentOf { state: CallParticipantsPagerState, mod: Modifier, aspectRatio: Float? -> - CallGrid( - items = state.callParticipants, - singleParticipantAspectRatio = aspectRatio, - modifier = mod, - itemKey = { it.callParticipantId } - ) { participant, itemModifier -> - val longPressModifier = if (!participant.recipient.isSelf && currentOnLongPress.value != null) { - var itemWindowOrigin by remember(participant.callParticipantId) { mutableStateOf(Offset.Zero) } - itemModifier - .onGloballyPositioned { coords -> itemWindowOrigin = coords.positionInRoot() } - .pointerInput(participant.callParticipantId) { - detectTapGestures( - onTap = { currentOnTap.value?.invoke() }, - onLongPress = { local -> currentOnLongPress.value?.invoke(participant, itemWindowOrigin + local) } - ) - } - } else { - itemModifier - } + VerticalPager( + state = pagerState, + modifier = modifier + .displayCutoutPadding() + .statusBarsPadding() + ) { page -> + when (page) { + 0 -> { + CallGrid( + items = callParticipantsPagerState.callParticipants, + singleParticipantAspectRatio = firstParticipantAR, + modifier = Modifier.fillMaxSize(), + itemKey = { it.callParticipantId } + ) { participant, itemModifier -> + val longPressModifier = if (!participant.recipient.isSelf && currentOnLongPress.value != null) { + var itemWindowOrigin by remember(participant.callParticipantId) { mutableStateOf(Offset.Zero) } + itemModifier + .onGloballyPositioned { coords -> itemWindowOrigin = coords.positionInRoot() } + .pointerInput(participant.callParticipantId) { + detectTapGestures( + onTap = { currentOnTap.value?.invoke() }, + onLongPress = { local -> currentOnLongPress.value?.invoke(participant, itemWindowOrigin + local) } + ) + } + } else { + itemModifier + } - RemoteParticipantContent( - participant = participant, - renderInPip = state.isRenderInPip, - raiseHandAllowed = false, - onInfoMoreInfoClick = null, - showAudioIndicator = state.callParticipants.size > 1, - modifier = longPressModifier - ) - } - } - } - - if (callParticipantsPagerState.callParticipants.size > 1) { - VerticalPager( - state = pagerState, - modifier = modifier - .displayCutoutPadding() - .statusBarsPadding() - ) { page -> - when (page) { - 0 -> { - callGridContent(callParticipantsPagerState, Modifier.fillMaxSize(), firstParticipantAR) - } - - 1 -> { RemoteParticipantContent( - participant = callParticipantsPagerState.focusedParticipant, + participant = participant, renderInPip = callParticipantsPagerState.isRenderInPip, raiseHandAllowed = false, onInfoMoreInfoClick = null, - modifier = Modifier.fillMaxSize() + showAudioIndicator = callParticipantsPagerState.callParticipants.size > 1, + modifier = longPressModifier ) } } + + 1 -> { + RemoteParticipantContent( + participant = callParticipantsPagerState.focusedParticipant, + renderInPip = callParticipantsPagerState.isRenderInPip, + raiseHandAllowed = false, + onInfoMoreInfoClick = null, + modifier = Modifier.fillMaxSize() + ) + } } - } else { - callGridContent(callParticipantsPagerState, modifier, firstParticipantAR) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreen.kt index d522c7f03e..154c302ee4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreen.kt @@ -104,8 +104,10 @@ fun CallScreen( savedLocalParticipantLandscape: Boolean = false, callScreenState: CallScreenState, callControlsState: CallControlsState, + callParticipantsPagerState: CallParticipantsPagerState, callScreenController: CallScreenController = CallScreenController.rememberCallScreenController( skipHiddenState = callControlsState.skipHiddenState, + hasMultipleRemoteParticipants = callParticipantsPagerState.callParticipants.size > 1, onControlsToggled = {}, callControlsState = callControlsState, callControlsListener = CallScreenControlsListener.Empty @@ -113,7 +115,6 @@ fun CallScreen( callScreenControlsListener: CallScreenControlsListener = CallScreenControlsListener.Empty, callScreenSheetDisplayListener: CallScreenSheetDisplayListener = CallScreenSheetDisplayListener.Empty, additionalActionsListener: AdditionalActionsListener = AdditionalActionsListener.Empty, - callParticipantsPagerState: CallParticipantsPagerState, pendingParticipantsListener: PendingParticipantsListener = PendingParticipantsListener.Empty, callParticipantUpdatePopupController: CallParticipantUpdatePopupController, overflowParticipants: List, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenController.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenController.kt index 52cf5403db..91b29b383f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenController.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenController.kt @@ -77,11 +77,13 @@ class CallScreenController private constructor( @Composable fun rememberCallScreenController( skipHiddenState: Boolean, + hasMultipleRemoteParticipants: Boolean, onControlsToggled: (Boolean) -> Unit, callControlsState: CallControlsState, callControlsListener: CallScreenControlsListener ): CallScreenController { val skip by rememberUpdatedState(skipHiddenState) + val hasMultipleRemoteParticipantsState = rememberUpdatedState(hasMultipleRemoteParticipants) val valueChangeOperation: (SheetValue) -> Boolean = remember { { !(it == SheetValue.Hidden && skip) @@ -130,7 +132,7 @@ class CallScreenController private constructor( val callParticipantsVerticalPagerState = rememberPagerState( initialPage = 0, - pageCount = { 2 } + pageCount = { if (hasMultipleRemoteParticipantsState.value) 2 else 1 } ) return remember(scaffoldState, callParticipantsVerticalPagerState, audioOutputPickerController) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/ComposeCallScreenMediator.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/ComposeCallScreenMediator.kt index 341123ff3a..a3054b79c7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/ComposeCallScreenMediator.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/ComposeCallScreenMediator.kt @@ -164,6 +164,7 @@ class ComposeCallScreenMediator(private val activity: WebRtcCallActivity, viewMo val callScreenController = CallScreenController.rememberCallScreenController( skipHiddenState = callControlsState.skipHiddenState, + hasMultipleRemoteParticipants = callParticipantsPagerState.callParticipants.size > 1, onControlsToggled = onControlsToggled, callControlsState = callControlsState, callControlsListener = callScreenControlsListener