mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-15 07:28:30 +00:00
Implement new call layout.
This commit is contained in:
committed by
jeffrey-signal
parent
e1454cfc6a
commit
a3e8ca8d33
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user