Implement new call layout.

This commit is contained in:
Alex Hart
2025-12-17 09:11:32 -04:00
committed by jeffrey-signal
parent e1454cfc6a
commit a3e8ca8d33
4 changed files with 369 additions and 243 deletions

View File

@@ -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
)
}
}

View File

@@ -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)
) {

View File

@@ -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<GroupCallReactionEvent>,
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<CallParticipant>,
scaffoldState: BottomSheetScaffoldState,
callControlsState: CallControlsState,
callScreenState: CallScreenState,
callScreenController: CallScreenController,
reactions: List<GroupCallReactionEvent>,
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,

View File

@@ -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)