diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallParticipantRenderer.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallParticipantRenderer.kt index cff7f5e86d..b5be1141b5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallParticipantRenderer.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallParticipantRenderer.kt @@ -10,9 +10,11 @@ import android.widget.FrameLayout import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.viewinterop.AndroidView import org.thoughtcrime.securesms.R @@ -35,12 +37,21 @@ fun CallParticipantRenderer( onToggleCameraDirection: () -> Unit = {} ) { if (LocalInspectionMode.current) { - Box(modifier.background(color = Color.Red)) + Box( + contentAlignment = Alignment.Center, + modifier = modifier.background(color = MaterialTheme.colorScheme.secondaryContainer) + ) { + Text( + text = "${callParticipant.callParticipantId.recipientId.toLong()}", + style = MaterialTheme.typography.titleLarge + ) + } } else { AndroidView( factory = { LayoutInflater.from(it).inflate(R.layout.call_participant_item, FrameLayout(it), false) as CallParticipantView }, - modifier = modifier.fillMaxSize().background(color = Color.Red), - onRelease = { it.releaseRenderer() } + modifier = modifier.fillMaxSize(), + onRelease = { it.releaseRenderer() }, + onReset = {} // Allows reuse in lazy lists ) { view -> view.setCallParticipant(callParticipant) view.setMirror(isLocalParticipant && callParticipant.cameraState.activeDirection == CameraState.Direction.FRONT) 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 4101b1b9ca..9ca56589ab 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 @@ -5,40 +5,138 @@ package org.thoughtcrime.securesms.components.webrtc.v2 -import android.view.LayoutInflater -import android.widget.FrameLayout +import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.viewinterop.AndroidView -import androidx.recyclerview.widget.RecyclerView -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.components.webrtc.WebRtcCallParticipantsRecyclerAdapter +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import org.signal.core.ui.compose.NightPreview +import org.signal.core.ui.compose.Previews +import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette import org.thoughtcrime.securesms.events.CallParticipant -import org.thoughtcrime.securesms.util.visible +import org.thoughtcrime.securesms.events.CallParticipantId +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.recipients.RecipientId /** - * Wrapper composable for the CallParticipants overflow recycler view. + * Replacement composable for the CallParticipants overflow recycler view. * * Displays a scrollable list of users that are in the call but are not displayed in the primary grid. */ @Composable fun CallParticipantsOverflow( + lineType: LayoutStrategyLineType, overflowParticipants: List, modifier: Modifier = Modifier ) { - val adapter = remember { WebRtcCallParticipantsRecyclerAdapter() } - - AndroidView( - factory = { - val view = LayoutInflater.from(it).inflate(R.layout.webrtc_call_participant_overflow_recycler, FrameLayout(it), false) as RecyclerView - view.adapter = adapter - view - }, - modifier = modifier, - update = { - it.visible = true - adapter.submitList(overflowParticipants) + if (lineType == LayoutStrategyLineType.ROW) { + LazyRow( + reverseLayout = true, + modifier = modifier, + contentPadding = PaddingValues(start = 16.dp, end = CallScreenMetrics.SmallRendererSize + 32.dp), + horizontalArrangement = spacedBy(4.dp) + ) { + appendItems(CallScreenMetrics.SmallRendererSize, overflowParticipants) } - ) + } else { + LazyColumn( + reverseLayout = true, + modifier = modifier, + contentPadding = PaddingValues(top = 16.dp, bottom = CallScreenMetrics.SmallRendererSize + 32.dp), + verticalArrangement = spacedBy(4.dp) + ) { + appendItems(CallScreenMetrics.SmallRendererSize, overflowParticipants) + } + } +} + +private fun LazyListScope.appendItems( + contentSize: Dp, + overflowParticipants: List +) { + items( + items = overflowParticipants, + key = { it.callParticipantId } + ) { participant -> + CallParticipantRenderer( + callParticipant = participant, + renderInPip = false, + modifier = Modifier + .size(contentSize) + .clip(CallScreenMetrics.SmallRendererShape) + ) + } +} + +@NightPreview +@Composable +private fun CallParticipantsOverflowPreview() { + Previews.Preview { + val participants = remember { + (1..10).map { + CallParticipant( + callParticipantId = CallParticipantId( + demuxId = 0, + recipientId = RecipientId.from(it.toLong()) + ), + recipient = Recipient( + isResolving = false, + chatColorsValue = ChatColorsPalette.UNKNOWN_CONTACT + ) + ) + } + } + + CallParticipantsOverflow( + lineType = LayoutStrategyLineType.ROW, + overflowParticipants = participants, + modifier = Modifier + .padding(vertical = 16.dp) + .height(CallScreenMetrics.SmallRendererSize) + .fillMaxWidth() + ) + } +} + +@NightPreview +@Composable +private fun CallParticipantsOverflowColumnPreview() { + Previews.Preview { + val participants = remember { + (1..10).map { + CallParticipant( + callParticipantId = CallParticipantId( + demuxId = 0, + recipientId = RecipientId.from(it.toLong()) + ), + recipient = Recipient( + isResolving = false, + chatColorsValue = ChatColorsPalette.UNKNOWN_CONTACT + ) + ) + } + } + + CallParticipantsOverflow( + lineType = LayoutStrategyLineType.COLUMN, + overflowParticipants = participants, + modifier = Modifier + .padding(horizontal = 16.dp) + .width(CallScreenMetrics.SmallRendererSize) + .fillMaxHeight() + ) + } } 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 7ac0d46e4a..b265559d0b 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 @@ -5,21 +5,38 @@ package org.thoughtcrime.securesms.components.webrtc.v2 -import android.content.res.Configuration -import android.view.LayoutInflater -import android.widget.FrameLayout +import androidx.compose.foundation.layout.Arrangement.spacedBy +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.VerticalPager +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Surface +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.viewinterop.AndroidView -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.components.webrtc.CallParticipantsLayout -import org.thoughtcrime.securesms.components.webrtc.CallParticipantsLayoutStrategies +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.window.core.layout.WindowHeightSizeClass +import androidx.window.core.layout.WindowWidthSizeClass +import org.signal.core.ui.compose.AllNightPreviews +import org.signal.core.ui.compose.Previews +import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette import org.thoughtcrime.securesms.events.CallParticipant +import org.thoughtcrime.securesms.events.CallParticipantId +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.recipients.RecipientId +import kotlin.math.min @Composable fun CallParticipantsPager( @@ -43,6 +60,7 @@ fun CallParticipantsPager( modifier = Modifier.fillMaxSize() ) } + 1 -> { CallParticipantRenderer( callParticipant = callParticipantsPagerState.focusedParticipant, @@ -60,32 +78,259 @@ fun CallParticipantsPager( } } +@OptIn(ExperimentalLayoutApi::class) @Composable fun CallParticipantsLayoutComponent( callParticipantsPagerState: CallParticipantsPagerState, modifier: Modifier = Modifier ) { - if (callParticipantsPagerState.focusedParticipant == null) { - return + val layoutStrategy = rememberRemoteParticipantsLayoutStrategy() + val count = min(callParticipantsPagerState.callParticipants.size, layoutStrategy.maxDeviceCount) + + val state = remember(count) { + layoutStrategy.buildStateForCount(count) } - val isPortrait = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT - - AndroidView( - factory = { - LayoutInflater.from(it).inflate(R.layout.webrtc_call_participants_layout, FrameLayout(it), false) as CallParticipantsLayout - }, - modifier = modifier + BoxWithConstraints( + contentAlignment = Alignment.Center, + modifier = modifier.padding(state.outerInsets) ) { - it.update( - callParticipantsPagerState.callParticipants, - callParticipantsPagerState.focusedParticipant, - callParticipantsPagerState.isRenderInPip, - isPortrait, - callParticipantsPagerState.hideAvatar, - 0, - CallParticipantsLayoutStrategies.getStrategy(isPortrait, true) + val width = maxWidth + val height = maxHeight + + val nLines = (count + state.maxItemsInEachLine - 1) / state.maxItemsInEachLine + + when (layoutStrategy.lineType) { + LayoutStrategyLineType.COLUMN -> { + ColumnBasedLayout( + containerWidth = width, + containerHeight = height, + numberOfLines = nLines, + numberOfParticipants = count, + state = state, + callParticipantsPagerState = callParticipantsPagerState + ) + } + + LayoutStrategyLineType.ROW -> { + RowBasedLayout( + containerWidth = width, + containerHeight = height, + numberOfLines = nLines, + numberOfParticipants = count, + state = state, + callParticipantsPagerState = callParticipantsPagerState + ) + } + } + } +} + +@Composable +private fun RowBasedLayout( + containerWidth: Dp, + containerHeight: Dp, + numberOfLines: Int, + numberOfParticipants: Int, + state: RemoteParticipantsLayoutState, + callParticipantsPagerState: CallParticipantsPagerState +) { + Row( + horizontalArrangement = spacedBy(state.innerInsets), + verticalAlignment = Alignment.CenterVertically + ) { + val batches = callParticipantsPagerState.callParticipants + .take(numberOfParticipants) + .chunked(state.maxItemsInEachLine) { + it.reversed() + } + + val lastParticipant = batches.last().last() + + batches.forEach { batch -> + Column( + verticalArrangement = spacedBy(state.innerInsets), + horizontalAlignment = Alignment.CenterHorizontally + ) { + batch.forEach { participant -> + + AutoSizedParticipant( + lineType = LayoutStrategyLineType.ROW, + containerWidth = containerWidth, + containerHeight = containerHeight, + numberOfLines = numberOfLines, + numberOfParticipants = numberOfParticipants, + isLastParticipant = lastParticipant == participant, + isRenderInPip = callParticipantsPagerState.isRenderInPip, + state = state, + participant = participant + ) + } + } + } + } +} + +@Composable +private fun ColumnBasedLayout( + containerWidth: Dp, + containerHeight: Dp, + numberOfLines: Int, + numberOfParticipants: Int, + state: RemoteParticipantsLayoutState, + callParticipantsPagerState: CallParticipantsPagerState +) { + Column( + verticalArrangement = spacedBy(state.innerInsets), + horizontalAlignment = Alignment.CenterHorizontally + ) { + val batches = callParticipantsPagerState.callParticipants + .take(numberOfParticipants) + .chunked(state.maxItemsInEachLine) + + val lastParticipant = batches.last().last() + + batches.forEach { batch -> + Row( + horizontalArrangement = spacedBy(state.innerInsets), + verticalAlignment = Alignment.CenterVertically + ) { + batch.forEach { participant -> + AutoSizedParticipant( + lineType = LayoutStrategyLineType.COLUMN, + containerWidth = containerWidth, + containerHeight = containerHeight, + numberOfLines = numberOfLines, + numberOfParticipants = numberOfParticipants, + isLastParticipant = lastParticipant == participant, + isRenderInPip = callParticipantsPagerState.isRenderInPip, + state = state, + participant = participant + ) + } + } + } + } +} + +@Composable +private fun AutoSizedParticipant( + lineType: LayoutStrategyLineType, + containerWidth: Dp, + containerHeight: Dp, + numberOfLines: Int, + numberOfParticipants: Int, + isLastParticipant: Boolean, + isRenderInPip: Boolean, + state: RemoteParticipantsLayoutState, + participant: CallParticipant +) { + val maxSize = when (lineType) { + LayoutStrategyLineType.COLUMN -> { + val itemMaximumHeight = (containerHeight - (state.innerInsets * (numberOfLines - 1))) / numberOfLines.toFloat() + val itemMaximumWidth = (containerWidth - (state.innerInsets * (state.maxItemsInEachLine - 1))) / state.maxItemsInEachLine.toFloat() + + DpSize(itemMaximumWidth, itemMaximumHeight) + } + + LayoutStrategyLineType.ROW -> { + val itemMaximumWidth = (containerWidth - (state.innerInsets * (numberOfLines - 1))) / numberOfLines.toFloat() + val itemMaximumHeight = (containerHeight - (state.innerInsets * (state.maxItemsInEachLine - 1))) / state.maxItemsInEachLine.toFloat() + + DpSize(itemMaximumWidth, itemMaximumHeight) + } + } + + val aspectRatio = state.aspectRatio ?: -1f + val sizeModifier = when { + aspectRatio > 0f -> + Modifier.size( + largestRectangleWithAspectRatio( + maxSize.width, + maxSize.height, + aspectRatio + ) + ) + + isLastParticipant && numberOfParticipants % 2 == 1 -> Modifier.fillMaxSize() + else -> Modifier.size(DpSize(maxSize.width, maxSize.height)) + } + + CallParticipantRenderer( + callParticipant = participant, + renderInPip = isRenderInPip, + modifier = sizeModifier + .clip(RoundedCornerShape(state.cornerRadius)) + ) +} + +private fun largestRectangleWithAspectRatio( + containerWidth: Dp, + containerHeight: Dp, + aspectRatio: Float +): DpSize { + val containerAspectRatio = containerWidth / containerHeight + + return if (containerAspectRatio > aspectRatio) { + DpSize( + width = containerHeight * aspectRatio, + height = containerHeight ) + } else { + DpSize( + width = containerWidth, + height = containerWidth / aspectRatio + ) + } +} + +@Composable +private fun rememberRemoteParticipantsLayoutStrategy(): RemoteParticipantsLayoutStrategy { + val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass + + return remember(windowSizeClass) { + when { + windowSizeClass.windowHeightSizeClass == WindowHeightSizeClass.COMPACT -> RemoteParticipantsLayoutStrategy.SmallLandscape() + windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT -> RemoteParticipantsLayoutStrategy.SmallPortrait() + windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.MEDIUM -> RemoteParticipantsLayoutStrategy.Medium() + else -> RemoteParticipantsLayoutStrategy.Large() + } + } +} + +@AllNightPreviews +@Composable +private fun CallParticipantsLayoutComponentPreview() { + Previews.Preview { + val participants = remember { + (1..5).map { + CallParticipant( + callParticipantId = CallParticipantId(0, RecipientId.from(it.toLong())), + recipient = Recipient( + isResolving = false, + chatColorsValue = ChatColorsPalette.UNKNOWN_CONTACT + ) + ) + } + } + + val state = remember { + CallParticipantsPagerState( + callParticipants = participants, + focusedParticipant = participants.first(), + isRenderInPip = false, + hideAvatar = false + ) + } + + Surface( + modifier = Modifier.fillMaxSize() + ) { + CallParticipantsLayoutComponent( + callParticipantsPagerState = state, + modifier = Modifier.fillMaxSize() + ) + } } } @@ -96,3 +341,76 @@ data class CallParticipantsPagerState( val isRenderInPip: Boolean = false, val hideAvatar: Boolean = false ) + +private sealed class RemoteParticipantsLayoutStrategy( + val maxDeviceCount: Int, + val lineType: LayoutStrategyLineType = LayoutStrategyLineType.COLUMN +) { + + abstract fun buildStateForCount(count: Int): RemoteParticipantsLayoutState + + class SmallLandscape : RemoteParticipantsLayoutStrategy(6, LayoutStrategyLineType.ROW) { + override fun buildStateForCount(count: Int): RemoteParticipantsLayoutState { + return RemoteParticipantsLayoutState( + outerInsets = if (count < 2) 0.dp else 16.dp, + innerInsets = if (count < 2) 0.dp else 12.dp, + cornerRadius = if (count < 2) 0.dp else CallScreenMetrics.FocusedRendererCornerSize, + maxItemsInEachLine = if (count < 3) 1 else 2 + ) + } + } + + class SmallPortrait : RemoteParticipantsLayoutStrategy(6) { + + override fun buildStateForCount(count: Int): RemoteParticipantsLayoutState { + return RemoteParticipantsLayoutState( + outerInsets = if (count < 2) 0.dp else 16.dp, + innerInsets = if (count < 2) 0.dp else 12.dp, + cornerRadius = if (count < 2) 0.dp else CallScreenMetrics.FocusedRendererCornerSize, + maxItemsInEachLine = if (count < 3) 1 else 2 + ) + } + } + + class Medium : RemoteParticipantsLayoutStrategy(9) { + override fun buildStateForCount(count: Int): RemoteParticipantsLayoutState { + return RemoteParticipantsLayoutState( + outerInsets = 24.dp, + innerInsets = 12.dp, + cornerRadius = CallScreenMetrics.FocusedRendererCornerSize, + aspectRatio = if (count < 2) 9 / 16f else 5 / 4f, + maxItemsInEachLine = when { + count < 3 -> 1 + count < 7 -> 2 + else -> 3 + } + ) + } + } + + class Large : RemoteParticipantsLayoutStrategy(12) { + override fun buildStateForCount(count: Int): RemoteParticipantsLayoutState { + return RemoteParticipantsLayoutState( + outerInsets = 24.dp, + innerInsets = 12.dp, + cornerRadius = CallScreenMetrics.FocusedRendererCornerSize, + aspectRatio = if (count < 2) 9 / 16f else 5 / 4f, + maxItemsInEachLine = when { + count < 4 -> 3 + count == 4 -> 2 + count < 7 -> 3 + else -> 4 + } + ) + } + } +} + +@Immutable +private data class RemoteParticipantsLayoutState( + val outerInsets: Dp, + val innerInsets: Dp, + val cornerRadius: Dp, + val maxItemsInEachLine: Int, + val aspectRatio: Float? = null +) 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 daed43cc41..6dc5c67647 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 @@ -18,14 +18,11 @@ 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.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer 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.size import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.material3.BottomSheetScaffold @@ -48,7 +45,6 @@ import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.positionInRoot import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -61,10 +57,12 @@ import org.thoughtcrime.securesms.components.webrtc.WebRtcLocalRenderState import org.thoughtcrime.securesms.components.webrtc.controls.RaiseHandSnackbar import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette import org.thoughtcrime.securesms.events.CallParticipant +import org.thoughtcrime.securesms.events.CallParticipantId import org.thoughtcrime.securesms.events.GroupCallRaiseHandEvent import org.thoughtcrime.securesms.events.GroupCallReactionEvent 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 kotlin.math.max import kotlin.math.round @@ -73,7 +71,6 @@ import kotlin.time.Duration.Companion.seconds private const val DRAG_HANDLE_HEIGHT = 22 private const val SHEET_TOP_PADDING = 9 private const val SHEET_BOTTOM_PADDING = 16 -private val OVERFLOW_ITEM_SIZE = 90.dp /** * In-App calling screen displaying controls, info, and participant camera feeds. @@ -400,18 +397,13 @@ private fun Viewport( if (isPortrait && isLargeGroupCall) { Row { CallParticipantsOverflow( + lineType = LayoutStrategyLineType.ROW, overflowParticipants = overflowParticipants, modifier = Modifier - .padding(top = 16.dp, start = 16.dp, bottom = 16.dp) - .height(OVERFLOW_ITEM_SIZE) + .padding(vertical = 16.dp) + .height(CallScreenMetrics.SmallRendererSize) .weight(1f) ) - - Spacer( - modifier = Modifier - .padding(top = 16.dp, bottom = 16.dp, end = 16.dp) - .size(OVERFLOW_ITEM_SIZE) - ) } } } @@ -419,15 +411,13 @@ private fun Viewport( if (!isPortrait && isLargeGroupCall) { Column { CallParticipantsOverflow( + lineType = LayoutStrategyLineType.COLUMN, overflowParticipants = overflowParticipants, modifier = Modifier - .width(OVERFLOW_ITEM_SIZE + 32.dp) + .padding(horizontal = 16.dp) + .width(CallScreenMetrics.SmallRendererSize) .weight(1f) ) - - Spacer( - modifier = Modifier.size(OVERFLOW_ITEM_SIZE) - ) } } } @@ -500,6 +490,7 @@ private fun CallScreenPreview() { val participants = remember { (1..10).map { CallParticipant( + callParticipantId = CallParticipantId(0, RecipientId.from(it.toLong())), recipient = Recipient( isResolving = false, chatColorsValue = ChatColorsPalette.UNKNOWN_CONTACT @@ -572,9 +563,3 @@ private fun CallScreenPreview() { ) } } - -data class SelfPictureInPictureDimensions( - val small: DpSize, - val expanded: DpSize, - val paddingValues: PaddingValues -) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenMetrics.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenMetrics.kt new file mode 100644 index 0000000000..b04c2f9460 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenMetrics.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.webrtc.v2 + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp + +object CallScreenMetrics { + val SmallRendererSize = 90.dp + val SmallRendererCornerSize = 24.dp + val ExpandedRendererCornerSize = 28.dp + val FocusedRendererCornerSize = 32.dp + + /** + * Shape of self renderer when in large group calls. + */ + val SmallRendererShape = RoundedCornerShape(SmallRendererCornerSize) + + /** + * Size of self renderer when in large group calls + */ + val SmallRendererDpSize = DpSize(SmallRendererSize, SmallRendererSize) + + /** + * Size of self renderer when in small group calls and 1:1 calls + */ + val NormalRendererDpSize = DpSize(90.dp, 160.dp) + + /** + * Size of self renderer after clicking on it to expand + */ + val ExpandedRendererDpSize = DpSize(170.dp, 300.dp) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/LayoutStrategyLineType.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/LayoutStrategyLineType.kt new file mode 100644 index 0000000000..e214059f26 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/LayoutStrategyLineType.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.webrtc.v2 + +enum class LayoutStrategyLineType { + COLUMN, + ROW +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/MoveableLocalVideoRenderer.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/MoveableLocalVideoRenderer.kt index 83c023d215..16b0ec659b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/MoveableLocalVideoRenderer.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/MoveableLocalVideoRenderer.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.displayCutoutPadding import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -39,7 +40,6 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpSize @@ -67,11 +67,11 @@ fun MoveableLocalVideoRenderer( val size = remember(localRenderState) { when (localRenderState) { WebRtcLocalRenderState.GONE -> DpSize.Zero - WebRtcLocalRenderState.SMALL_RECTANGLE -> DpSize(90.dp, 160.dp) - WebRtcLocalRenderState.SMALLER_RECTANGLE -> DpSize(90.dp, 90.dp) + WebRtcLocalRenderState.SMALL_RECTANGLE -> CallScreenMetrics.NormalRendererDpSize + WebRtcLocalRenderState.SMALLER_RECTANGLE -> CallScreenMetrics.SmallRendererDpSize WebRtcLocalRenderState.LARGE -> DpSize.Zero WebRtcLocalRenderState.LARGE_NO_VIDEO -> DpSize.Zero - WebRtcLocalRenderState.EXPANDED -> DpSize(170.dp, 300.dp) + WebRtcLocalRenderState.EXPANDED -> CallScreenMetrics.ExpandedRendererDpSize WebRtcLocalRenderState.FOCUSED -> DpSize.Unspecified } } @@ -81,6 +81,7 @@ fun MoveableLocalVideoRenderer( .fillMaxSize() .then(modifier) .statusBarsPadding() + .displayCutoutPadding() ) { val targetSize = size.let { if (it == DpSize.Unspecified) { @@ -167,9 +168,9 @@ fun MoveableLocalVideoRenderer( @Composable private fun animateClip(localRenderState: WebRtcLocalRenderState): State { val targetDp = when (localRenderState) { - WebRtcLocalRenderState.FOCUSED -> 32.dp - WebRtcLocalRenderState.EXPANDED -> 28.dp - else -> 24.dp + WebRtcLocalRenderState.FOCUSED -> CallScreenMetrics.FocusedRendererCornerSize + WebRtcLocalRenderState.EXPANDED -> CallScreenMetrics.ExpandedRendererCornerSize + else -> CallScreenMetrics.SmallRendererCornerSize } return animateDpAsState(targetValue = targetDp)