From a3e8ca8d339e4673be6ccfdea401ef0c2f3d9ec8 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Wed, 17 Dec 2025 09:11:32 -0400 Subject: [PATCH] Implement new call layout. --- .../webrtc/v2/CallElementsLayout.kt | 224 +++++++++++ .../webrtc/v2/CallParticipantsOverflow.kt | 4 +- .../components/webrtc/v2/CallScreen.kt | 377 +++++++----------- .../webrtc/v2/PendingParticipants.kt | 7 +- 4 files changed, 369 insertions(+), 243 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallElementsLayout.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallElementsLayout.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallElementsLayout.kt new file mode 100644 index 0000000000..129fe48192 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallElementsLayout.kt @@ -0,0 +1,224 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.webrtc.v2 + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.offset +import androidx.window.core.layout.WindowSizeClass +import org.signal.core.ui.compose.AllNightPreviews +import org.signal.core.ui.compose.Previews +import org.thoughtcrime.securesms.components.webrtc.WebRtcLocalRenderState +import org.thoughtcrime.securesms.events.CallParticipant +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.recipients.RecipientId + +@Composable +fun CallElementsLayout( + callGridSlot: @Composable () -> Unit, + pictureInPictureSlot: @Composable () -> Unit, + reactionsSlot: @Composable () -> Unit, + raiseHandSlot: @Composable () -> Unit, + callLinkBarSlot: @Composable () -> Unit, + callOverflowSlot: @Composable () -> Unit, + bottomInset: Dp, + bottomSheetWidth: Dp, + localRenderState: WebRtcLocalRenderState, + modifier: Modifier = Modifier +) { + val isPortrait = LocalConfiguration.current.orientation != Configuration.ORIENTATION_LANDSCAPE + val isCompactPortrait = !currentWindowAdaptiveInfo().windowSizeClass.isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND) + + @Composable + fun Bars() { + Column { + raiseHandSlot() + callLinkBarSlot() + } + } + + val density = LocalDensity.current + val pipSizePx = with(density) { + (rememberSelfPipSize(localRenderState) + DpSize(32.dp, 0.dp)).toSize() + } + + val bottomInsetPx = with(density) { + if (isCompactPortrait) 0 else bottomInset.roundToPx() + } + + val bottomSheetWidthPx = with(density) { + bottomSheetWidth.roundToPx() + } + + Layout( + contents = listOf(::Bars, callGridSlot, reactionsSlot, pictureInPictureSlot, callOverflowSlot), + modifier = if (isCompactPortrait) { Modifier.padding(bottom = bottomInset).then(modifier) } else modifier + ) { measurables, constraints -> + val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0) + val overflowPlaceables = measurables[4].map { it.measure(looseConstraints) } + val constrainedHeightOffset = if (isPortrait) overflowPlaceables.maxOfOrNull { it.height } ?: 0 else 0 + val constrainedWidthOffset = if (isPortrait) { 0 } else overflowPlaceables.maxOfOrNull { it.width } ?: 0 + + val nonOverflowConstraints = looseConstraints.offset(horizontal = -constrainedWidthOffset, vertical = -constrainedHeightOffset) + val gridPlaceables = measurables[1].map { it.measure(nonOverflowConstraints) } + + val barConstraints = if (bottomInsetPx > constrainedHeightOffset) { + looseConstraints.offset(-constrainedWidthOffset, -bottomInsetPx) + } else { + nonOverflowConstraints + } + + val barsPlaceables = measurables[0].map { it.measure(barConstraints) } + + val barsHeightOffset = barsPlaceables.sumOf { it.height } + val reactionsConstraints = barConstraints.offset(vertical = -barsHeightOffset) + val reactionsPlaceables = measurables[2].map { it.measure(reactionsConstraints) } + + val pictureInPictureConstraints: Constraints = when (localRenderState) { + WebRtcLocalRenderState.GONE, WebRtcLocalRenderState.SMALLER_RECTANGLE, WebRtcLocalRenderState.LARGE, WebRtcLocalRenderState.LARGE_NO_VIDEO, WebRtcLocalRenderState.FOCUSED -> constraints + WebRtcLocalRenderState.SMALL_RECTANGLE, WebRtcLocalRenderState.EXPANDED -> { + val hasBars = barsPlaceables.sumOf { it.width } > 0 + if (hasBars) { + looseConstraints.offset(vertical = reactionsConstraints.maxHeight - looseConstraints.maxHeight) + } else if (bottomInsetPx > 0) { + if (looseConstraints.maxWidth - pipSizePx.width - pipSizePx.width - bottomSheetWidthPx < 0) { + looseConstraints.offset(vertical = -bottomInsetPx) + } else { + looseConstraints + } + } else { + looseConstraints + } + } + } + + val pictureInPicturePlaceables = measurables[3].map { it.measure(pictureInPictureConstraints) } + + layout(looseConstraints.maxWidth, looseConstraints.maxHeight) { + overflowPlaceables.forEach { + if (isPortrait) { + it.place(0, looseConstraints.maxHeight - it.height) + } else { + it.place(looseConstraints.maxWidth - it.width, 0) + } + } + + gridPlaceables.forEach { + it.place(0, 0) + } + + barsPlaceables.forEach { + it.place(0, barConstraints.maxHeight - it.height) + } + + reactionsPlaceables.forEach { + it.place(0, 0) + } + + pictureInPicturePlaceables.forEach { + it.place(0, 0) + } + } + } +} + +@AllNightPreviews +@Composable +private fun CallElementsLayoutPreview() { + val metrics = rememberCallScreenMetrics() + val isPortrait = LocalConfiguration.current.orientation != Configuration.ORIENTATION_LANDSCAPE + val localRenderState = WebRtcLocalRenderState.SMALL_RECTANGLE + + Previews.Preview { + CallElementsLayout( + callGridSlot = { + Box( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + .background(color = Color.Gray) + ) + }, + pictureInPictureSlot = { + MoveableLocalVideoRenderer( + localParticipant = CallParticipant( + recipient = Recipient(id = RecipientId.from(1L), isResolving = false, systemContactName = "Test") + ), + onClick = {}, + onFocusLocalParticipantClick = {}, + onToggleCameraDirectionClick = {}, + localRenderState = localRenderState + ) + }, + reactionsSlot = { + Box( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + .background(color = Color.Yellow) + ) + }, + raiseHandSlot = { + Box( + modifier = Modifier + .padding(16.dp) + .height(48.dp) + .fillMaxWidth() + .background(color = Color.Green) + ) + }, + callLinkBarSlot = { + Box( + modifier = Modifier + .padding(16.dp) + .height(48.dp) + .fillMaxWidth() + .background(color = Color.Blue) + ) + }, + callOverflowSlot = { + Box( + modifier = Modifier + .padding(16.dp) + .then( + if (isPortrait) { + Modifier + .fillMaxWidth() + .height(metrics.overflowParticipantRendererAvatarSize) + } else { + Modifier + .fillMaxHeight() + .width(metrics.overflowParticipantRendererAvatarSize) + } + ) + .background(color = Color.Red) + ) + }, + bottomInset = 120.dp, + bottomSheetWidth = 640.dp, + localRenderState = localRenderState + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallParticipantsOverflow.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallParticipantsOverflow.kt index 1a7477230d..a687314ad3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallParticipantsOverflow.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallParticipantsOverflow.kt @@ -48,7 +48,7 @@ fun CallParticipantsOverflow( if (lineType == LayoutStrategyLineType.ROW) { LazyRow( reverseLayout = true, - modifier = modifier, + modifier = Modifier.fillMaxWidth().then(modifier), contentPadding = PaddingValues(start = 16.dp, end = rendererSize + 32.dp), horizontalArrangement = spacedBy(4.dp) ) { @@ -57,7 +57,7 @@ fun CallParticipantsOverflow( } else { LazyColumn( reverseLayout = true, - modifier = modifier, + modifier = Modifier.fillMaxHeight().then(modifier), contentPadding = PaddingValues(top = 16.dp, bottom = rendererSize + 32.dp), verticalArrangement = spacedBy(4.dp) ) { 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 c9d592991c..1dd15f7078 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 @@ -17,8 +17,6 @@ import androidx.compose.animation.togetherWith import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -28,7 +26,6 @@ import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.material3.BottomSheetDefaults import androidx.compose.material3.BottomSheetScaffold -import androidx.compose.material3.BottomSheetScaffoldState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme @@ -53,7 +50,6 @@ import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.positionInRoot import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.window.core.layout.WindowSizeClass import kotlinx.coroutines.delay @@ -76,8 +72,10 @@ import org.thoughtcrime.securesms.events.WebRtcViewModel import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.ringrtc.CameraState +import org.thoughtcrime.securesms.service.webrtc.PendingParticipantCollection import kotlin.math.max import kotlin.math.round +import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds private const val DRAG_HANDLE_HEIGHT = 22 @@ -163,6 +161,15 @@ fun CallScreen( additionalActionsPopupState.display = callScreenState.displayAdditionalActionsDialog + val hideSheet by rememberUpdatedState(newValue = scaffoldState.bottomSheetState.currentValue == SheetValue.PartiallyExpanded && !callControlsState.skipHiddenState && !callScreenState.isDisplayingControlMenu()) + LaunchedEffect(callScreenController.restartTimerRequests, hideSheet) { + if (hideSheet) { + delay(5.seconds) + scaffoldState.bottomSheetState.hide() + onControlsToggled(false) + } + } + BoxWithConstraints { val maxHeight = constraints.maxHeight val maxSheetHeight = round(constraints.maxHeight * 0.66f) @@ -234,69 +241,6 @@ fun CallScreen( label = "animate-as-state" ) - // Self-pip bottom inset should be based off of: - // A. The container width - // B. The sheet width - // A - B / 2 gives you the gutter width. - // If the pip in its current state would be bigger than the gutter width (accounting for padding) - // then we need to apply the inset. - - val selfPipHorizontalPadding = 32.dp - val shouldNotApplyBottomPaddingToViewPort = currentWindowAdaptiveInfo().windowSizeClass.isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND) - val selfPipBottomInset: Dp = if (shouldNotApplyBottomPaddingToViewPort && localRenderState != WebRtcLocalRenderState.SMALLER_RECTANGLE) { - val containerWidth = maxWidth - val sheetWidth = BottomSheetDefaults.SheetMaxWidth - val widthOfPip = rememberSelfPipSize(localRenderState).width - - if (containerWidth <= sheetWidth) { - padding - } else { - val spaceRemaining: Dp = (containerWidth - sheetWidth) / 2f - selfPipHorizontalPadding - - if (spaceRemaining > widthOfPip) { - 0.dp - } else { - padding - } - } - } else { - 0.dp - } - - // Reactions/raised hands need bottom inset to stay above the bottom sheet, - // UNLESS the overflow row is present (portrait + large group call), in which case - // the reactions sit above the overflow row naturally. - val isPortrait = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT - val hasOverflowRow = isPortrait && overflowParticipants.size > 1 - val reactionsAndRaisesHandBottomInset = if (shouldNotApplyBottomPaddingToViewPort && !hasOverflowRow) { - padding - } else { - 0.dp - } - - Viewport( - localParticipant = localParticipant, - localRenderState = localRenderState, - webRtcCallState = webRtcCallState, - callParticipantsPagerState = callParticipantsPagerState, - overflowParticipants = overflowParticipants, - scaffoldState = scaffoldState, - callControlsState = callControlsState, - callScreenState = callScreenState, - onPipClick = onLocalPictureInPictureClicked, - onPipFocusClick = onLocalPictureInPictureFocusClicked, - onControlsToggled = onControlsToggled, - callScreenController = callScreenController, - onToggleCameraDirection = callScreenControlsListener::onCameraDirectionChanged, - selfPipBottomInset = selfPipBottomInset, - modifier = if (shouldNotApplyBottomPaddingToViewPort) { - Modifier - } else Modifier.padding(bottom = padding), - reactions = reactions, - raiseHandSnackbar = raiseHandSnackbar, - reactionsAndRaisesHandBottomInset = reactionsAndRaisesHandBottomInset - ) - val onCallInfoClick: () -> Unit = { scope.launch { if (scaffoldState.bottomSheetState.currentValue == SheetValue.Expanded) { @@ -307,21 +251,15 @@ fun CallScreen( } } - if (webRtcCallState.isPassedPreJoin) { - AnimatedVisibility( - visible = scaffoldState.bottomSheetState.targetValue != SheetValue.Hidden, - enter = fadeIn(), - exit = fadeOut() - ) { - CallScreenTopBar( - callRecipient = callRecipient, - callStatus = callScreenState.callStatus, - onNavigationClick = onNavigationClick, - onCallInfoClick = onCallInfoClick, - modifier = Modifier.padding(bottom = padding) + val isCompactPortrait = !currentWindowAdaptiveInfo().windowSizeClass.isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND) + if (webRtcCallState.isPreJoinOrNetworkUnavailable) { + if (localParticipant.isVideoEnabled) { + LargeLocalVideoRenderer( + localParticipant = localParticipant, + modifier = if (isCompactPortrait) Modifier.padding(bottom = padding) else Modifier ) } - } else { + CallScreenPreJoinOverlay( callRecipient = callRecipient, callStatus = callScreenState.callStatus, @@ -332,133 +270,24 @@ fun CallScreen( isMoreThanOneCameraAvailable = localParticipant.isMoreThanOneCameraAvailable, modifier = Modifier.padding(bottom = padding) ) - } + } else if (webRtcCallState.inOngoingCall && callParticipantsPagerState.callParticipants.isEmpty()) { + if (localParticipant.isVideoEnabled) { + LargeLocalVideoRenderer( + localParticipant = localParticipant, + modifier = if (isCompactPortrait) Modifier.padding(bottom = padding) else Modifier + ) - // This content lives "above" the controls sheet and includes raised hands, status updates, etc. - Box( - modifier = Modifier - .fillMaxSize() - .padding(bottom = padding) - ) { - AnimatedCallStateUpdate( - callControlsChange = callScreenState.callControlsChange, - modifier = Modifier - .align(Alignment.BottomCenter) - .padding(bottom = 20.dp) - ) - - val state = remember(callScreenState.pendingParticipantsState) { - callScreenState.pendingParticipantsState - } - - if (state != null) { - PendingParticipants( - pendingParticipantsState = state, - pendingParticipantsListener = pendingParticipantsListener + CallScreenTopBar( + callRecipient = callRecipient, + callStatus = callScreenState.callStatus, + onNavigationClick = onNavigationClick, + onCallInfoClick = onCallInfoClick, + modifier = Modifier.padding(bottom = padding) ) } - - if (callScreenState.isParticipantUpdatePopupEnabled) { - CallParticipantUpdatePopup( - controller = callParticipantUpdatePopupController, - modifier = Modifier - .statusBarsPadding() - .fillMaxWidth() - ) - } - } - } - } - - CallScreenDialog(callScreenDialogType, onCallScreenDialogDismissed) -} - -@Composable -private fun ReactionsAndRaiseHand( - reactions: List, - raiseHandSnackbar: @Composable (Modifier) -> Unit, - modifier: Modifier = Modifier -) { - Column( - modifier = modifier - .fillMaxSize() - .padding(bottom = 20.dp) - ) { - CallScreenReactionsContainer( - reactions = reactions, - modifier = Modifier.weight(1f) - ) - - raiseHandSnackbar( - Modifier - ) - } -} - -/** - * Primary 'viewport' which will either render content above or behind the controls depending on - * whether we are in landscape or portrait. - */ -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun Viewport( - localParticipant: CallParticipant, - localRenderState: WebRtcLocalRenderState, - webRtcCallState: WebRtcViewModel.State, - callParticipantsPagerState: CallParticipantsPagerState, - overflowParticipants: List, - scaffoldState: BottomSheetScaffoldState, - callControlsState: CallControlsState, - callScreenState: CallScreenState, - callScreenController: CallScreenController, - reactions: List, - raiseHandSnackbar: @Composable (Modifier) -> Unit, - onPipClick: () -> Unit, - onPipFocusClick: () -> Unit, - onControlsToggled: (Boolean) -> Unit, - onToggleCameraDirection: () -> Unit, - selfPipBottomInset: Dp, - reactionsAndRaisesHandBottomInset: Dp, - modifier: Modifier = Modifier -) { - val isEmptyOngoingCall = webRtcCallState.inOngoingCall && callParticipantsPagerState.callParticipants.isEmpty() - if (webRtcCallState.isPreJoinOrNetworkUnavailable || isEmptyOngoingCall) { - if (localParticipant.isVideoEnabled) { - LargeLocalVideoRenderer( - localParticipant = localParticipant, - modifier = modifier - ) - } - - return - } - - val isLargeGroupCall = overflowParticipants.size > 1 - if (webRtcCallState.isPassedPreJoin) { - val isPortrait = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT - val scope = rememberCoroutineScope() - - val hideSheet by rememberUpdatedState(newValue = scaffoldState.bottomSheetState.currentValue == SheetValue.PartiallyExpanded && !callControlsState.skipHiddenState && !callScreenState.isDisplayingControlMenu()) - LaunchedEffect(callScreenController.restartTimerRequests, hideSheet) { - if (hideSheet) { - delay(5.seconds) - scaffoldState.bottomSheetState.hide() - onControlsToggled(false) - } - } - - val callScreenMetrics = rememberCallScreenMetrics() - BlurContainer( - isBlurred = localRenderState == WebRtcLocalRenderState.FOCUSED, - modifier = modifier.fillMaxWidth() - ) { - Row(modifier = Modifier.fillMaxSize()) { - Column( - modifier = Modifier.weight(1f) - ) { - Box( - modifier = Modifier.fillMaxWidth().weight(1f) - ) { + } else if (webRtcCallState.isPassedPreJoin) { + CallElementsLayout( + callGridSlot = { CallParticipantsPager( callParticipantsPagerState = callParticipantsPagerState, pagerState = callScreenController.callParticipantsVerticalPagerState, @@ -473,52 +302,109 @@ private fun Viewport( enabled = !callControlsState.skipHiddenState ) ) - - ReactionsAndRaiseHand( - reactions = reactions, - raiseHandSnackbar = raiseHandSnackbar, - modifier = Modifier.padding(bottom = reactionsAndRaisesHandBottomInset) + }, + pictureInPictureSlot = { + MoveableLocalVideoRenderer( + localParticipant = localParticipant, + localRenderState = localRenderState, + onClick = onLocalPictureInPictureClicked, + onToggleCameraDirectionClick = callScreenControlsListener::onCameraDirectionChanged, + onFocusLocalParticipantClick = onLocalPictureInPictureFocusClicked, + modifier = Modifier.fillMaxSize() ) - } + }, + reactionsSlot = { + CallScreenReactionsContainer( + reactions = reactions, + modifier = Modifier.fillMaxSize() + ) + }, + raiseHandSlot = { + raiseHandSnackbar(Modifier.fillMaxWidth().padding(bottom = 16.dp)) + }, + callLinkBarSlot = { + val state = remember(callScreenState.pendingParticipantsState) { + callScreenState.pendingParticipantsState + } - if (isPortrait && isLargeGroupCall) { - Row { - CallParticipantsOverflow( - lineType = LayoutStrategyLineType.ROW, - overflowParticipants = overflowParticipants, - modifier = Modifier - .padding(vertical = 16.dp) - .height(callScreenMetrics.overflowParticipantRendererSize) + if (state != null) { + PendingParticipants( + pendingParticipantsState = state, + pendingParticipantsListener = pendingParticipantsListener, + modifier = Modifier.fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(bottom = 16.dp) ) } - } - } + }, + callOverflowSlot = { + val metrics = rememberCallScreenMetrics() + if (overflowParticipants.isNotEmpty()) { + val lineType = if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) { + LayoutStrategyLineType.COLUMN + } else { + LayoutStrategyLineType.ROW + } - if (!isPortrait && isLargeGroupCall) { - Column { - CallParticipantsOverflow( - lineType = LayoutStrategyLineType.COLUMN, - overflowParticipants = overflowParticipants, - modifier = Modifier - .padding(horizontal = 16.dp) - .width(callScreenMetrics.overflowParticipantRendererSize) - ) - } + CallParticipantsOverflow( + lineType = lineType, + overflowParticipants = overflowParticipants, + modifier = when (lineType) { + LayoutStrategyLineType.COLUMN -> + Modifier + .padding(horizontal = 16.dp) + .width(metrics.overflowParticipantRendererSize) + + LayoutStrategyLineType.ROW -> + Modifier + .padding(vertical = 16.dp) + .height(metrics.overflowParticipantRendererSize) + } + ) + } + }, + bottomInset = padding, + bottomSheetWidth = BottomSheetDefaults.SheetMaxWidth, + localRenderState = localRenderState, + modifier = Modifier.fillMaxSize() + ) + + AnimatedVisibility( + visible = scaffoldState.bottomSheetState.targetValue != SheetValue.Hidden, + enter = fadeIn(), + exit = fadeOut() + ) { + CallScreenTopBar( + callRecipient = callRecipient, + callStatus = callScreenState.callStatus, + onNavigationClick = onNavigationClick, + onCallInfoClick = onCallInfoClick, + modifier = Modifier.padding(bottom = padding) + ) } } + + Box(modifier = Modifier.fillMaxSize().padding(bottom = padding)) { + AnimatedCallStateUpdate( + callControlsChange = callScreenState.callControlsChange, + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = 20.dp) + ) + } } } - if (webRtcCallState.inOngoingCall) { - MoveableLocalVideoRenderer( - localParticipant = localParticipant, - localRenderState = localRenderState, - onClick = onPipClick, - onToggleCameraDirectionClick = onToggleCameraDirection, - onFocusLocalParticipantClick = onPipFocusClick, - modifier = modifier.padding(bottom = selfPipBottomInset) + if (callScreenState.isParticipantUpdatePopupEnabled) { + CallParticipantUpdatePopup( + controller = callParticipantUpdatePopupController, + modifier = Modifier + .statusBarsPadding() + .fillMaxWidth() ) } + + CallScreenDialog(callScreenDialogType, onCallScreenDialogDismissed) } /** @@ -592,7 +478,20 @@ private fun CallScreenPreview() { isRemoteVideoOffer = false, isInPipMode = false, callScreenState = CallScreenState( - callStatus = "Connecting..." + callStatus = "Connecting...", + pendingParticipantsState = PendingParticipantsState( + pendingParticipantCollection = PendingParticipantCollection( + participantMap = mapOf( + RecipientId.from(2) to PendingParticipantCollection.Entry( + recipient = Recipient(id = RecipientId.from(2L), isResolving = false, systemContactName = "Miles Morales"), + state = PendingParticipantCollection.State.PENDING, + stateChangeAt = System.currentTimeMillis().milliseconds, + denialCount = 0 + ) + ) + ), + isInPipMode = false + ) ), callControlsState = CallControlsState( displayMicToggle = true, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/PendingParticipants.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/PendingParticipants.kt index 5612e638f2..2babe1f3f9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/PendingParticipants.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/PendingParticipants.kt @@ -10,6 +10,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier import androidx.compose.ui.viewinterop.AndroidView import org.signal.core.ui.compose.DayNightPreviews import org.signal.core.ui.compose.Previews @@ -22,7 +23,8 @@ import org.thoughtcrime.securesms.service.webrtc.PendingParticipantCollection @Composable fun PendingParticipants( pendingParticipantsState: PendingParticipantsState, - pendingParticipantsListener: PendingParticipantsListener + pendingParticipantsListener: PendingParticipantsListener, + modifier: Modifier = Modifier ) { if (pendingParticipantsState.isInPipMode) { return @@ -34,7 +36,8 @@ fun PendingParticipants( hasDisplayedContent = true AndroidView( - ::PendingParticipantsView + ::PendingParticipantsView, + modifier = modifier ) { view -> view.listener = pendingParticipantsListener view.applyState(pendingParticipantsState.pendingParticipantCollection)