diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt index a61a6a4f058..826fe112610 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt @@ -82,6 +82,7 @@ import com.wire.android.ui.common.topappbar.NavigationIconType import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar import com.wire.android.ui.common.visbility.rememberVisibilityState import com.wire.android.ui.home.conversations.PermissionPermanentlyDeniedDialogState +import com.wire.android.ui.home.conversationslist.model.Membership import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireDimensions @@ -90,6 +91,7 @@ import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.kalium.logic.data.call.CallStatus import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.QualifiedID import java.util.Locale @Suppress("ParameterWrapping") @@ -119,42 +121,30 @@ fun OngoingCallScreen( } } - with(sharedCallingViewModel.callState) { - OngoingCallContent( - conversationId = conversationId, - conversationName = conversationName, - participants = participants, - isMuted = isMuted ?: true, - isCameraOn = isCameraOn, - isSpeakerOn = isSpeakerOn, - isCbrEnabled = isCbrEnabled, - isOnFrontCamera = isOnFrontCamera, - protocolInfo = protocolInfo, - mlsVerificationStatus = mlsVerificationStatus, - proteusVerificationStatus = proteusVerificationStatus, - shouldShowDoubleTapToast = ongoingCallViewModel.shouldShowDoubleTapToast, - toggleSpeaker = sharedCallingViewModel::toggleSpeaker, - toggleMute = sharedCallingViewModel::toggleMute, - hangUpCall = { sharedCallingViewModel.hangUpCall { activity.finishAndRemoveTask() } }, - toggleVideo = sharedCallingViewModel::toggleVideo, - flipCamera = sharedCallingViewModel::flipCamera, - setVideoPreview = sharedCallingViewModel::setVideoPreview, - clearVideoPreview = sharedCallingViewModel::clearVideoPreview, - onCollapse = { activity.moveTaskToBack(true) }, - requestVideoStreams = ongoingCallViewModel::requestVideoStreams, - hideDoubleTapToast = ongoingCallViewModel::hideDoubleTapToast, - onCameraPermissionPermanentlyDenied = { - permissionPermanentlyDeniedDialogState.show( - PermissionPermanentlyDeniedDialogState.Visible( - title = R.string.app_permission_dialog_title, - description = R.string.camera_permission_dialog_description - ) + OngoingCallContent( + callState = sharedCallingViewModel.callState, + shouldShowDoubleTapToast = ongoingCallViewModel.shouldShowDoubleTapToast, + toggleSpeaker = sharedCallingViewModel::toggleSpeaker, + toggleMute = sharedCallingViewModel::toggleMute, + hangUpCall = { sharedCallingViewModel.hangUpCall { activity.finishAndRemoveTask() } }, + toggleVideo = sharedCallingViewModel::toggleVideo, + flipCamera = sharedCallingViewModel::flipCamera, + setVideoPreview = sharedCallingViewModel::setVideoPreview, + clearVideoPreview = sharedCallingViewModel::clearVideoPreview, + onCollapse = { activity.moveTaskToBack(true) }, + requestVideoStreams = ongoingCallViewModel::requestVideoStreams, + hideDoubleTapToast = ongoingCallViewModel::hideDoubleTapToast, + onCameraPermissionPermanentlyDenied = { + permissionPermanentlyDeniedDialogState.show( + PermissionPermanentlyDeniedDialogState.Visible( + title = R.string.app_permission_dialog_title, + description = R.string.camera_permission_dialog_description ) - } - ) - BackHandler { - activity.moveTaskToBack(true) + ) } + ) + BackHandler { + activity.moveTaskToBack(true) } PermissionPermanentlyDeniedDialog( @@ -216,18 +206,8 @@ private fun HandleSendingVideoFeed( @OptIn(ExperimentalMaterial3Api::class) @Composable private fun OngoingCallContent( - conversationId: ConversationId, - conversationName: ConversationName?, - participants: List, - isMuted: Boolean, - isCameraOn: Boolean, - isOnFrontCamera: Boolean, - isSpeakerOn: Boolean, - isCbrEnabled: Boolean, + callState: CallState, shouldShowDoubleTapToast: Boolean, - protocolInfo: Conversation.ProtocolInfo?, - mlsVerificationStatus: Conversation.VerificationStatus?, - proteusVerificationStatus: Conversation.VerificationStatus?, toggleSpeaker: () -> Unit, toggleMute: () -> Unit, hangUpCall: () -> Unit, @@ -239,7 +219,7 @@ private fun OngoingCallContent( hideDoubleTapToast: () -> Unit, onCameraPermissionPermanentlyDenied: () -> Unit, requestVideoStreams: (participants: List) -> Unit -) { +) = with(callState) { val sheetInitialValue = SheetValue.PartiallyExpanded val sheetState = rememberStandardBottomSheetState( @@ -274,7 +254,7 @@ private fun OngoingCallContent( sheetContent = { CallingControls( conversationId = conversationId, - isMuted = isMuted, + isMuted = isMuted ?: true, isCameraOn = isCameraOn, isOnFrontCamera = isOnFrontCamera, isSpeakerOn = isSpeakerOn, @@ -329,6 +309,7 @@ private fun OngoingCallContent( if (shouldOpenFullScreen) { hideDoubleTapToast() FullScreenTile( + callState = callState, selectedParticipant = selectedParticipantForFullScreen, height = this@BoxWithConstraints.maxHeight - dimensions().spacing4x, closeFullScreen = { @@ -336,13 +317,15 @@ private fun OngoingCallContent( }, onBackButtonClicked = { shouldOpenFullScreen = !shouldOpenFullScreen - } + }, + setVideoPreview = setVideoPreview, + clearVideoPreview = clearVideoPreview, ) } else { VerticalCallingPager( participants = participants, isSelfUserCameraOn = isCameraOn, - isSelfUserMuted = isMuted, + isSelfUserMuted = isMuted ?: true, contentHeight = this@BoxWithConstraints.maxHeight, onSelfVideoPreviewCreated = setVideoPreview, onSelfClearVideoPreview = clearVideoPreview, @@ -469,22 +452,23 @@ private fun CallingControls( } } -@PreviewMultipleThemes @Composable -fun PreviewOngoingCallScreen() = WireTheme { +fun PreviewOngoingCallContent(participants: List) { OngoingCallContent( - conversationId = ConversationId("conversationId", "domain"), - conversationName = ConversationName.Known("Conversation Name"), - participants = emptyList(), - isMuted = false, - isCameraOn = false, - isOnFrontCamera = false, - isSpeakerOn = false, - isCbrEnabled = false, + callState = CallState( + conversationId = ConversationId("conversationId", "domain"), + conversationName = ConversationName.Known("Conversation Name"), + participants = participants, + isMuted = false, + isCameraOn = false, + isOnFrontCamera = false, + isSpeakerOn = false, + isCbrEnabled = false, + protocolInfo = null, + mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, + proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, + ), shouldShowDoubleTapToast = false, - protocolInfo = null, - mlsVerificationStatus = null, - proteusVerificationStatus = null, toggleSpeaker = {}, toggleMute = {}, hangUpCall = {}, @@ -499,8 +483,45 @@ fun PreviewOngoingCallScreen() = WireTheme { ) } +@PreviewMultipleThemes +@Composable +fun PreviewOngoingCallScreenConnecting() = WireTheme { + PreviewOngoingCallContent(participants = emptyList()) +} + +@PreviewMultipleThemes +@Composable +fun PreviewOngoingCallScreen_2Participants() = WireTheme { + PreviewOngoingCallContent(participants = buildPreviewParticipantsList(2)) +} + +@PreviewMultipleThemes +@Composable +fun PreviewOngoingCallScreen_8Participants() = WireTheme { + PreviewOngoingCallContent(participants = buildPreviewParticipantsList(8)) +} + @PreviewMultipleThemes @Composable fun PreviewOngoingCallTopBar() = WireTheme { OngoingCallTopBar("Default", true, null, null, null) { } } + +fun buildPreviewParticipantsList(count: Int = 10) = buildList { + repeat(count) { index -> + add( + UICallParticipant( + id = QualifiedID("id_$index", ""), + clientId = "client_id_$index", + name = "Participant $index", + isSpeaking = index % 3 == 1, + isMuted = index % 3 == 2, + hasEstablishedAudio = index % 3 != 2, + isCameraOn = false, + isSharingScreen = false, + avatar = null, + membership = Membership.Admin, + ) + ) + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/fullscreen/FullScreenTile.kt b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/fullscreen/FullScreenTile.kt index 1caadcf8213..870f8425033 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/fullscreen/FullScreenTile.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/fullscreen/FullScreenTile.kt @@ -17,6 +17,7 @@ */ package com.wire.android.ui.calling.ongoing.fullscreen +import android.view.View import androidx.activity.compose.BackHandler import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Box @@ -37,22 +38,28 @@ import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R -import com.wire.android.ui.calling.SharedCallingViewModel +import com.wire.android.ui.calling.CallState import com.wire.android.ui.calling.ongoing.OngoingCallViewModel.Companion.DOUBLE_TAP_TOAST_DISPLAY_TIME +import com.wire.android.ui.calling.ongoing.buildPreviewParticipantsList import com.wire.android.ui.calling.ongoing.participantsview.ParticipantTile import com.wire.android.ui.common.dimensions +import com.wire.android.ui.theme.WireTheme import com.wire.android.util.ui.PreviewMultipleThemes +import com.wire.kalium.logic.data.id.ConversationId import kotlinx.coroutines.delay @Composable fun FullScreenTile( - sharedCallingViewModel: SharedCallingViewModel = hiltViewModel(), + callState: CallState, selectedParticipant: SelectedParticipant, height: Dp, closeFullScreen: (offset: Offset) -> Unit, - onBackButtonClicked: () -> Unit + onBackButtonClicked: () -> Unit, + setVideoPreview: (View) -> Unit, + clearVideoPreview: () -> Unit, + modifier: Modifier = Modifier, + contentPadding: Dp = dimensions().spacing4x, ) { var shouldShowDoubleTapToast by remember { mutableStateOf(false) } @@ -60,10 +67,10 @@ fun FullScreenTile( onBackButtonClicked() } - sharedCallingViewModel.callState.participants.find { + callState.participants.find { it.id == selectedParticipant.userId && it.clientId == selectedParticipant.clientId }?.let { - Box { + Box(modifier = modifier) { ParticipantTile( modifier = Modifier .fillMaxWidth() @@ -74,26 +81,23 @@ fun FullScreenTile( ) } .height(height) - .padding( - start = dimensions().spacing4x, - end = dimensions().spacing4x - ), + .padding(contentPadding), participantTitleState = it, isSelfUser = selectedParticipant.isSelfUser, isSelfUserCameraOn = if (selectedParticipant.isSelfUser) { - sharedCallingViewModel.callState.isCameraOn + callState.isCameraOn } else { it.isCameraOn }, isSelfUserMuted = if (selectedParticipant.isSelfUser) { - sharedCallingViewModel.callState.isMuted!! + callState.isMuted!! } else { it.isMuted }, shouldFill = false, isZoomingEnabled = true, - onSelfUserVideoPreviewCreated = sharedCallingViewModel::setVideoPreview, - onClearSelfUserVideoPreview = sharedCallingViewModel::clearVideoPreview + onSelfUserVideoPreviewCreated = setVideoPreview, + onClearSelfUserVideoPreview = clearVideoPreview ) LaunchedEffect(Unit) { delay(200) @@ -116,11 +120,22 @@ fun FullScreenTile( @PreviewMultipleThemes @Composable -fun PreviewFullScreenVideoCall() { +fun PreviewFullScreenTile() = WireTheme { + val participants = buildPreviewParticipantsList(1) FullScreenTile( - selectedParticipant = SelectedParticipant(), - height = 100.dp, + callState = CallState( + conversationId = ConversationId("id", "domain"), + participants = participants, + ), + selectedParticipant = SelectedParticipant( + userId = participants.first().id, + clientId = participants.first().clientId, + isSelfUser = false, + ), + height = 800.dp, closeFullScreen = {}, - onBackButtonClicked = {} + onBackButtonClicked = {}, + setVideoPreview = {}, + clearVideoPreview = {}, ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/participantsview/ParticipantTile.kt b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/participantsview/ParticipantTile.kt index 8a97d8eadcd..fa13601d846 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/participantsview/ParticipantTile.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/participantsview/ParticipantTile.kt @@ -15,21 +15,20 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ +@file:Suppress("TooManyFunctions") package com.wire.android.ui.calling.ongoing.participantsview import android.view.View import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.widget.FrameLayout -import androidx.compose.foundation.Canvas +import androidx.compose.foundation.border import androidx.compose.foundation.gestures.detectTransformGestures import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ContentAlpha import androidx.compose.material3.Icon @@ -45,24 +44,26 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha -import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.layoutId import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.min import androidx.compose.ui.viewinterop.AndroidView import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension +import androidx.constraintlayout.compose.atMost import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner @@ -72,9 +73,11 @@ import com.wire.android.R import com.wire.android.model.UserAvatarData import com.wire.android.ui.calling.model.UICallParticipant import com.wire.android.ui.common.UserProfileAvatar +import com.wire.android.ui.common.UserProfileAvatarType import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.common.dimensions import com.wire.android.ui.home.conversationslist.model.Membership +import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireTypography import com.wire.android.util.ui.PreviewMultipleThemes @@ -82,37 +85,41 @@ import com.wire.kalium.logic.data.id.QualifiedID @Composable fun ParticipantTile( - modifier: Modifier, participantTitleState: UICallParticipant, - onGoingCallTileUsernameMaxWidth: Dp = 350.dp, - avatarSize: Dp = dimensions().onGoingCallUserAvatarSize, isSelfUser: Boolean, - shouldFill: Boolean = true, - isZoomingEnabled: Boolean = false, isSelfUserMuted: Boolean, isSelfUserCameraOn: Boolean, onSelfUserVideoPreviewCreated: (view: View) -> Unit, + modifier: Modifier = Modifier, + shouldFill: Boolean = true, + isZoomingEnabled: Boolean = false, onClearSelfUserVideoPreview: () -> Unit ) { - val defaultUserName = stringResource(id = R.string.calling_participant_tile_default_user_name) - val alpha = - if (participantTitleState.hasEstablishedAudio) ContentAlpha.high else ContentAlpha.medium + val alpha = if (participantTitleState.hasEstablishedAudio) ContentAlpha.high else ContentAlpha.medium Surface( - modifier = modifier, + modifier = modifier + .thenIf(participantTitleState.isSpeaking, activeSpeakerBorderModifier), color = colorsScheme().callingParticipantTileBackgroundColor, - shape = RoundedCornerShape(dimensions().corner6x), + shape = RoundedCornerShape(if (participantTitleState.isSpeaking) dimensions().corner8x else dimensions().corner3x), ) { - ConstraintLayout { - val (avatar, userName, muteIcon) = createRefs() + val (avatar, bottomRow) = createRefs() + val maxAvatarSize = dimensions().onGoingCallUserAvatarSize + val activeSpeakerBorderPadding = dimensions().spacing6x AvatarTile( modifier = Modifier - .fillMaxSize() .alpha(alpha) - .constrainAs(avatar) { }, + .padding(top = activeSpeakerBorderPadding) + .constrainAs(avatar) { + top.linkTo(parent.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + bottom.linkTo(bottomRow.top) + width = Dimension.fillToConstraints.atMost(maxAvatarSize) + height = Dimension.fillToConstraints.atMost(maxAvatarSize + activeSpeakerBorderPadding) + }, avatar = UserAvatarData(participantTitleState.avatar), - avatarSize = avatarSize ) if (isSelfUser) { @@ -132,40 +139,89 @@ fun ParticipantTile( ) } - MicrophoneTile( + BottomRow( + participantTitleState = participantTitleState, + isSelfUser = isSelfUser, + isSelfUserMuted = isSelfUserMuted, modifier = Modifier - .padding( - start = dimensions().spacing8x, - bottom = dimensions().spacing8x + .padding( // move by the size of the active speaker border + start = dimensions().spacing6x, + end = dimensions().spacing6x, + bottom = dimensions().spacing6x, ) - .constrainAs(muteIcon) { + .constrainAs(bottomRow) { bottom.linkTo(parent.bottom) start.linkTo(parent.start) - }, + end.linkTo(parent.end) + } + ) + } + } +} + +private fun Modifier.thenIf(condition: Boolean, other: Modifier): Modifier = if (condition) this.then(other) else this + +private val activeSpeakerBorderModifier + @Composable get() = Modifier + .border( + width = dimensions().spacing3x, + shape = RoundedCornerShape(dimensions().corner8x), + color = colorsScheme().primary + ) + .border( + width = dimensions().spacing6x, + shape = RoundedCornerShape(dimensions().corner9x), + color = colorsScheme().background + ) + +@Composable +private fun BottomRow( + participantTitleState: UICallParticipant, + isSelfUser: Boolean, + isSelfUserMuted: Boolean, + modifier: Modifier = Modifier, +) { + val defaultUserName = stringResource(id = R.string.calling_participant_tile_default_user_name) + Layout( + modifier = modifier, + content = { + MicrophoneTile( + modifier = Modifier + .padding(end = dimensions().spacing8x) + .layoutId("muteIcon"), isMuted = if (isSelfUser) isSelfUserMuted else participantTitleState.isMuted, hasEstablishedAudio = participantTitleState.hasEstablishedAudio ) - UsernameTile( modifier = Modifier - .padding(bottom = dimensions().spacing8x) - .constrainAs(userName) { - bottom.linkTo(parent.bottom) - start.linkTo(parent.start) - end.linkTo((parent.end)) - } - .widthIn(max = onGoingCallTileUsernameMaxWidth), + .layoutId("username"), name = participantTitleState.name ?: defaultUserName, isSpeaking = participantTitleState.isSpeaking, hasEstablishedAudio = participantTitleState.hasEstablishedAudio ) + }, + measurePolicy = { measurables, constraints -> + val muteIconPlaceable = measurables.firstOrNull { it.layoutId == "muteIcon" } + ?.measure(constraints.copy(minWidth = 0, minHeight = 0)) + val muteIconWidth = muteIconPlaceable?.width ?: 0 + val maxUsernameWidth = constraints.maxWidth - muteIconWidth + val usernamePlaceable = measurables.first { it.layoutId == "username" } + .measure(constraints.copy(minWidth = 0, minHeight = 0, maxWidth = maxUsernameWidth)) + + layout(constraints.maxWidth, usernamePlaceable.height) { + muteIconPlaceable?.placeRelative(0, 0) + if (usernamePlaceable.width < constraints.maxWidth - 2 * muteIconWidth) { // can fit in center + usernamePlaceable.placeRelative((constraints.maxWidth - usernamePlaceable.width) / 2, 0) + } else { // needs to take all remaining space + usernamePlaceable.placeRelative(muteIconWidth, 0) + } + } } - TileBorder(participantTitleState.isSpeaking) - } + ) } @Composable -private fun cleanUpRendererIfNeeded(videoRenderer: VideoRenderer) { +private fun CleanUpRendererIfNeeded(videoRenderer: VideoRenderer) { val lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current DisposableEffect(videoRenderer, lifecycleOwner) { val observer = LifecycleEventObserver { _, event -> @@ -183,28 +239,6 @@ private fun cleanUpRendererIfNeeded(videoRenderer: VideoRenderer) { } } -@Composable -private fun TileBorder(isSpeaking: Boolean) { - if (isSpeaking) { - val color = MaterialTheme.wireColorScheme.primary - val strokeWidth = dimensions().corner8x - val cornerRadius = dimensions().corner10x - - Canvas(modifier = Modifier.fillMaxSize()) { - val canvasQuadrantSize = size - drawRoundRect( - color = color, - size = canvasQuadrantSize, - style = Stroke(width = strokeWidth.toPx()), - cornerRadius = CornerRadius( - x = cornerRadius.toPx(), - y = cornerRadius.toPx() - ) - ) - } - } -} - @Composable private fun CameraPreview( isCameraOn: Boolean, @@ -268,7 +302,7 @@ private fun OthersVideoRenderer( } } - cleanUpRendererIfNeeded(videoRenderer) + CleanUpRendererIfNeeded(videoRenderer) AndroidView( modifier = Modifier @@ -306,79 +340,59 @@ private fun OthersVideoRenderer( @Composable private fun AvatarTile( - modifier: Modifier, avatar: UserAvatarData, - avatarSize: Dp + modifier: Modifier = Modifier, ) { - Column( + BoxWithConstraints( modifier = modifier, - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally + contentAlignment = Alignment.Center, ) { + val size = min(maxWidth, maxHeight) UserProfileAvatar( - size = avatarSize, - avatarData = avatar + padding = dimensions().spacing0x, + size = size, + avatarData = avatar, + type = UserProfileAvatarType.WithoutIndicators, ) } } @Composable private fun UsernameTile( - modifier: Modifier, name: String, isSpeaking: Boolean, - hasEstablishedAudio: Boolean + hasEstablishedAudio: Boolean, + modifier: Modifier = Modifier, ) { - val color = if (isSpeaking) MaterialTheme.wireColorScheme.primary else Color.Black - val nameLabelColor = if (hasEstablishedAudio) Color.White else colorsScheme().secondaryText - - ConstraintLayout(modifier = modifier) { - val (nameLabel, connectingLabel) = createRefs() + val color = if (isSpeaking) colorsScheme().primary else colorsScheme().callingParticipantNameBackground + val nameLabelColor = + when { + isSpeaking -> colorsScheme().onPrimary + hasEstablishedAudio -> colorsScheme().callingParticipantNameText + else -> colorsScheme().callingParticipantNameConnectingText + } - Surface( - modifier = Modifier.constrainAs(nameLabel) { - bottom.linkTo(parent.bottom) - start.linkTo(parent.start) - end.linkTo(connectingLabel.start) - }, - shape = RoundedCornerShape( - topStart = dimensions().corner4x, - bottomStart = dimensions().corner4x, - topEnd = if (hasEstablishedAudio) dimensions().corner4x else 0.dp, - bottomEnd = if (hasEstablishedAudio) dimensions().corner4x else 0.dp, - ), - color = color + Surface( + modifier = modifier, + shape = RoundedCornerShape(dimensions().corner3x), + color = color, + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(dimensions().spacing4x), + modifier = Modifier.padding(dimensions().spacing4x) ) { Text( color = nameLabelColor, style = MaterialTheme.wireTypography.label01, - modifier = Modifier.padding(dimensions().spacing4x), text = name, maxLines = 1, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f, fill = false) ) - } - if (!hasEstablishedAudio) { - Surface( - modifier = Modifier.constrainAs(connectingLabel) { - start.linkTo(nameLabel.end) - top.linkTo(nameLabel.top) - bottom.linkTo(nameLabel.bottom) - }, - shape = RoundedCornerShape( - topEnd = dimensions().corner4x, - bottomEnd = dimensions().corner4x - ), - color = color - ) { + if (!hasEstablishedAudio) { Text( - color = colorsScheme().error, + color = colorsScheme().callingParticipantError, style = MaterialTheme.wireTypography.label01, - modifier = Modifier.padding( - top = dimensions().spacing4x, - bottom = dimensions().spacing4x, - end = dimensions().spacing4x - ), text = stringResource(id = R.string.participant_tile_call_connecting_label), maxLines = 1, ) @@ -389,43 +403,55 @@ private fun UsernameTile( @Composable private fun MicrophoneTile( - modifier: Modifier, isMuted: Boolean, - hasEstablishedAudio: Boolean + hasEstablishedAudio: Boolean, + modifier: Modifier = Modifier, ) { if (isMuted && hasEstablishedAudio) { Surface( modifier = modifier, color = Color.Black, - shape = RoundedCornerShape(dimensions().corner6x) + shape = RoundedCornerShape(dimensions().corner3x) ) { Icon( modifier = Modifier - .padding(dimensions().spacing4x), + .padding(dimensions().spacing3x) + .size(dimensions().spacing16x), imageVector = ImageVector.vectorResource(id = R.drawable.ic_participant_muted), - tint = MaterialTheme.wireColorScheme.muteButtonColor, + tint = MaterialTheme.wireColorScheme.callingParticipantError, contentDescription = stringResource(R.string.content_description_calling_participant_muted) ) } } } -@Preview("Default view") +private enum class PreviewTileShape(val width: Dp, val height: Dp) { + Regular(width = 175.dp, height = 140.dp), + Tall(width = 140.dp, height = 175.dp), + Wide(width = 175.dp, height = 80.dp), +} + @Composable -fun PreviewParticipantTile() { +private fun PreviewParticipantTile( + longName: Boolean = false, + isMuted: Boolean = false, + isSpeaking: Boolean = false, + hasEstablishedAudio: Boolean = true, + shape: PreviewTileShape = PreviewTileShape.Wide, +) { ParticipantTile( - modifier = Modifier.height(300.dp), + modifier = Modifier.size(width = shape.width, height = shape.height), participantTitleState = UICallParticipant( id = QualifiedID("", ""), clientId = "client-id", - name = "user name", - isMuted = true, - isSpeaking = false, + name = if (longName) "long user name to be displayed in participant tile during a call" else "user name", + isMuted = isMuted, + isSpeaking = isSpeaking, isCameraOn = false, isSharingScreen = false, avatar = null, membership = Membership.Admin, - hasEstablishedAudio = true + hasEstablishedAudio = hasEstablishedAudio, ), onClearSelfUserVideoPreview = {}, onSelfUserVideoPreviewCreated = {}, @@ -435,54 +461,80 @@ fun PreviewParticipantTile() { ) } +// ------ connecting ------ + @PreviewMultipleThemes @Composable -fun PreviewParticipantTalking() { - ParticipantTile( - modifier = Modifier.height(300.dp), - participantTitleState = UICallParticipant( - id = QualifiedID("", ""), - clientId = "client-id", - name = "long user name to be displayed in participant tile during a call", - isMuted = false, - isSpeaking = true, - isCameraOn = false, - isSharingScreen = false, - avatar = null, - membership = Membership.Admin, - hasEstablishedAudio = true - ), - onClearSelfUserVideoPreview = {}, - onSelfUserVideoPreviewCreated = {}, - isSelfUser = false, - isSelfUserMuted = false, - isSelfUserCameraOn = false - ) +fun PreviewParticipantConnecting() = WireTheme { + PreviewParticipantTile(shape = PreviewTileShape.Regular, hasEstablishedAudio = false) } @PreviewMultipleThemes @Composable -fun PreviewParticipantConnecting() { - ParticipantTile( - modifier = Modifier - .height(350.dp) - .width(200.dp), - participantTitleState = UICallParticipant( - id = QualifiedID("", ""), - clientId = "client-id", - name = "Oussama2", - isMuted = true, - isSpeaking = false, - isCameraOn = false, - isSharingScreen = false, - avatar = null, - membership = Membership.Admin, - hasEstablishedAudio = false - ), - onClearSelfUserVideoPreview = {}, - onSelfUserVideoPreviewCreated = {}, - isSelfUser = false, - isSelfUserMuted = false, - isSelfUserCameraOn = false - ) +fun PreviewParticipantLongNameConnecting() = WireTheme { + PreviewParticipantTile(shape = PreviewTileShape.Regular, hasEstablishedAudio = false, longName = true) +} + +@PreviewMultipleThemes +@Composable +fun PreviewParticipantTallLongNameConnecting() = WireTheme { + PreviewParticipantTile(shape = PreviewTileShape.Tall, hasEstablishedAudio = false) +} + +@PreviewMultipleThemes +@Composable +fun PreviewParticipantWideLongNameConnecting() = WireTheme { + PreviewParticipantTile(shape = PreviewTileShape.Wide, hasEstablishedAudio = false) +} + +// ------ muted ------ + +@PreviewMultipleThemes +@Composable +fun PreviewParticipantMuted() = WireTheme { + PreviewParticipantTile(shape = PreviewTileShape.Regular, isMuted = true) +} + +@PreviewMultipleThemes +@Composable +fun PreviewParticipantLongNameMuted() = WireTheme { + PreviewParticipantTile(shape = PreviewTileShape.Regular, isMuted = true, longName = true) +} + +@PreviewMultipleThemes +@Composable +fun PreviewParticipantTallLongNameMuted() = WireTheme { + PreviewParticipantTile(shape = PreviewTileShape.Tall, isMuted = true) +} + +@PreviewMultipleThemes +@Composable +fun PreviewParticipantWideLongNameMuted() = WireTheme { + PreviewParticipantTile(shape = PreviewTileShape.Wide, isMuted = true) +} + +// ------ talking ------ + +@PreviewMultipleThemes +@Composable +fun PreviewParticipantTalking() = WireTheme { + PreviewParticipantTile(shape = PreviewTileShape.Regular, isSpeaking = true) +} + +@PreviewMultipleThemes +@Composable +fun PreviewParticipantLongNameTalking() = WireTheme { + PreviewParticipantTile(shape = PreviewTileShape.Regular, isSpeaking = true, longName = true) +} + +@PreviewMultipleThemes +@Composable +fun PreviewParticipantTallLongNameTalking() = WireTheme { + PreviewParticipantTile(shape = PreviewTileShape.Tall, isSpeaking = true) +} + +@PreviewMultipleThemes +@Composable +fun PreviewParticipantWideLongNameTalking() = WireTheme { + PreviewParticipantTile(shape = PreviewTileShape.Wide, isSpeaking = true) } diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/participantsview/ParticipantsTiles.kt b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/participantsview/ParticipantsTiles.kt index 53abf8f60c2..c187a608bfe 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/participantsview/ParticipantsTiles.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/participantsview/ParticipantsTiles.kt @@ -19,7 +19,6 @@ package com.wire.android.ui.calling.ongoing.participantsview import android.view.View -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -42,18 +41,19 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.wire.android.ui.calling.model.UICallParticipant +import com.wire.android.ui.calling.ongoing.buildPreviewParticipantsList import com.wire.android.ui.calling.ongoing.fullscreen.SelectedParticipant import com.wire.android.ui.calling.ongoing.participantsview.gridview.GroupCallGrid import com.wire.android.ui.calling.ongoing.participantsview.horizentalview.CallingHorizontalView import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.common.dimensions +import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireDimensions import com.wire.android.util.ui.PreviewMultipleThemes private const val MAX_TILES_PER_PAGE = 8 private const val MAX_ITEMS_FOR_HORIZONTAL_VIEW = 3 -@OptIn(ExperimentalFoundationApi::class) @Composable fun VerticalCallingPager( participants: List, @@ -63,10 +63,11 @@ fun VerticalCallingPager( onSelfVideoPreviewCreated: (view: View) -> Unit, onSelfClearVideoPreview: () -> Unit, requestVideoStreams: (participants: List) -> Unit, - onDoubleTap: (selectedParticipant: SelectedParticipant) -> Unit + onDoubleTap: (selectedParticipant: SelectedParticipant) -> Unit, + modifier: Modifier = Modifier, ) { Column( - modifier = Modifier + modifier = modifier .fillMaxWidth() .height(contentHeight) ) { @@ -156,11 +157,10 @@ private fun pagesCount(size: Int): Int { } else pages } -@PreviewMultipleThemes @Composable -fun PreviewVerticalCallingPager() { +private fun PreviewVerticalCallingPager(participants: List) { VerticalCallingPager( - participants = listOf(), + participants = participants, isSelfUserMuted = false, isSelfUserCameraOn = false, contentHeight = 800.dp, @@ -170,3 +170,15 @@ fun PreviewVerticalCallingPager() { onDoubleTap = { } ) } + +@PreviewMultipleThemes +@Composable +fun PreviewVerticalCallingPagerHorizontalView() = WireTheme { + PreviewVerticalCallingPager(participants = buildPreviewParticipantsList(MAX_ITEMS_FOR_HORIZONTAL_VIEW)) +} + +@PreviewMultipleThemes +@Composable +fun PreviewVerticalCallingPagerGrid() = WireTheme { + PreviewVerticalCallingPager(participants = buildPreviewParticipantsList(MAX_TILES_PER_PAGE)) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/participantsview/gridview/CallingGridView.kt b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/participantsview/gridview/CallingGridView.kt index d27da12ae48..81e02697057 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/participantsview/gridview/CallingGridView.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/participantsview/gridview/CallingGridView.kt @@ -19,31 +19,29 @@ package com.wire.android.ui.calling.ongoing.participantsview.gridview import android.view.View -import androidx.compose.animation.core.tween import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.height import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.times import com.wire.android.ui.calling.model.UICallParticipant +import com.wire.android.ui.calling.ongoing.buildPreviewParticipantsList import com.wire.android.ui.calling.ongoing.fullscreen.SelectedParticipant import com.wire.android.ui.calling.ongoing.participantsview.ParticipantTile import com.wire.android.ui.common.dimensions -import com.wire.android.ui.home.conversationslist.model.Membership -import com.wire.android.ui.theme.wireDimensions -import com.wire.kalium.logic.data.id.QualifiedID +import com.wire.android.ui.theme.WireTheme +import com.wire.android.util.ui.PreviewMultipleThemes @OptIn(ExperimentalFoundationApi::class) @Composable @@ -55,15 +53,25 @@ fun GroupCallGrid( contentHeight: Dp, onSelfVideoPreviewCreated: (view: View) -> Unit, onSelfClearVideoPreview: () -> Unit, - onDoubleTap: (selectedParticipant: SelectedParticipant) -> Unit + onDoubleTap: (selectedParticipant: SelectedParticipant) -> Unit, + modifier: Modifier = Modifier, + contentPadding: Dp = dimensions().spacing4x, + spacedBy: Dp = dimensions().spacing2x, ) { - val config = LocalConfiguration.current - + // We need the number of tiles rows needed to calculate their height + val numberOfTilesRows = remember(participants.size) { + tilesRowsCount(participants.size) + } + val tileHeight = remember(participants.size, contentHeight, contentPadding, spacedBy) { + val heightAvailableForItems = contentHeight - 2 * contentPadding - (numberOfTilesRows - 1) * spacedBy + heightAvailableForItems / numberOfTilesRows + } LazyVerticalGrid( + modifier = modifier, userScrollEnabled = false, - contentPadding = PaddingValues(MaterialTheme.wireDimensions.spacing4x), - horizontalArrangement = Arrangement.spacedBy(MaterialTheme.wireDimensions.spacing2x), - verticalArrangement = Arrangement.spacedBy(MaterialTheme.wireDimensions.spacing2x), + contentPadding = PaddingValues(contentPadding), + horizontalArrangement = Arrangement.spacedBy(spacedBy), + verticalArrangement = Arrangement.spacedBy(spacedBy), columns = GridCells.Fixed(NUMBER_OF_GRID_CELLS) ) { @@ -77,22 +85,6 @@ fun GroupCallGrid( val isSelfUser = remember(pageIndex, participants.first()) { pageIndex == 0 && participants.first() == participant } - // We need the number of tiles rows needed to calculate their height - val numberOfTilesRows = remember(participants.size) { - tilesRowsCount(participants.size) - } - - // if we have more than 6 participants then we reduce avatar size - val userAvatarSize = if (participants.size <= 6 || config.screenHeightDp > MIN_SCREEN_HEIGHT) { - dimensions().onGoingCallUserAvatarSize - } else { - dimensions().onGoingCallUserAvatarMinimizedSize - } - - val spacing4x = dimensions().spacing4x - val tileHeight = remember(numberOfTilesRows) { - (contentHeight - spacing4x) / numberOfTilesRows - } ParticipantTile( modifier = Modifier @@ -110,10 +102,8 @@ fun GroupCallGrid( ) } .height(tileHeight) - .animateItemPlacement(tween(durationMillis = 200)), + .animateItem(), participantTitleState = participant, - onGoingCallTileUsernameMaxWidth = dimensions().onGoingCallTileUsernameMaxWidth, - avatarSize = userAvatarSize, isSelfUser = isSelfUser, isSelfUserMuted = isSelfUserMuted, isSelfUserCameraOn = isSelfUserCameraOn, @@ -137,44 +127,37 @@ private fun getContentType( ) = if (isCameraOn || isSharingScreen) "videoRender" else null private const val NUMBER_OF_GRID_CELLS = 2 -private const val MIN_SCREEN_HEIGHT = 800 -@Preview @Composable -fun PreviewGroupCallGrid() { - GroupCallGrid( - participants = listOf( - UICallParticipant( - id = QualifiedID("", ""), - clientId = "clientId", - name = "name", - isMuted = false, - isSpeaking = false, - isCameraOn = false, - isSharingScreen = false, - avatar = null, - membership = Membership.Admin, - hasEstablishedAudio = true - ), - UICallParticipant( - id = QualifiedID("", ""), - clientId = "clientId", - name = "name", - isMuted = false, - isSpeaking = false, - isCameraOn = false, - isSharingScreen = false, - avatar = null, - membership = Membership.Admin, - hasEstablishedAudio = true - ) - ), - contentHeight = 800.dp, - pageIndex = 0, - isSelfUserMuted = true, - isSelfUserCameraOn = false, - onSelfVideoPreviewCreated = { }, - onSelfClearVideoPreview = { }, - onDoubleTap = { } - ) +private fun PreviewGroupCallGrid(participants: List, modifier: Modifier = Modifier) { + Box(modifier = modifier.height(800.dp)) { + GroupCallGrid( + participants = participants, + pageIndex = 0, + isSelfUserMuted = false, + isSelfUserCameraOn = false, + contentHeight = 800.dp, + onSelfVideoPreviewCreated = {}, + onSelfClearVideoPreview = {}, + onDoubleTap = { } + ) + } +} + +@PreviewMultipleThemes +@Composable +fun PreviewGroupCallGrid_4Participants() = WireTheme { + PreviewGroupCallGrid(buildPreviewParticipantsList(4)) +} + +@PreviewMultipleThemes +@Composable +fun PreviewGroupCallGrid_6Participants() = WireTheme { + PreviewGroupCallGrid(buildPreviewParticipantsList(6)) +} + +@PreviewMultipleThemes +@Composable +fun PreviewGroupCallGrid_8Participants() = WireTheme { + PreviewGroupCallGrid(buildPreviewParticipantsList(8)) } diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/participantsview/horizentalview/CallingHorizontalView.kt b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/participantsview/horizentalview/CallingHorizontalView.kt index 8825b452963..34535476799 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/participantsview/horizentalview/CallingHorizontalView.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/participantsview/horizentalview/CallingHorizontalView.kt @@ -19,32 +19,29 @@ package com.wire.android.ui.calling.ongoing.participantsview.horizentalview import android.view.View -import androidx.compose.animation.core.tween -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.times import com.wire.android.ui.calling.model.UICallParticipant +import com.wire.android.ui.calling.ongoing.buildPreviewParticipantsList import com.wire.android.ui.calling.ongoing.fullscreen.SelectedParticipant import com.wire.android.ui.calling.ongoing.participantsview.ParticipantTile import com.wire.android.ui.common.dimensions -import com.wire.android.ui.home.conversationslist.model.Membership -import com.wire.android.ui.theme.wireDimensions +import com.wire.android.ui.theme.WireTheme import com.wire.android.util.ui.PreviewMultipleThemes -import com.wire.kalium.logic.data.id.QualifiedID -@OptIn(ExperimentalFoundationApi::class) @Composable fun CallingHorizontalView( participants: List, @@ -54,16 +51,20 @@ fun CallingHorizontalView( contentHeight: Dp, onSelfVideoPreviewCreated: (view: View) -> Unit, onSelfClearVideoPreview: () -> Unit, - onDoubleTap: (selectedParticipant: SelectedParticipant) -> Unit + onDoubleTap: (selectedParticipant: SelectedParticipant) -> Unit, + modifier: Modifier = Modifier, + contentPadding: Dp = dimensions().spacing4x, + spacedBy: Dp = dimensions().spacing2x, ) { - + val tileHeight = remember(participants.size, contentHeight, contentPadding, spacedBy) { + val heightAvailableForItems = contentHeight - 2 * contentPadding - (participants.size - 1) * spacedBy + heightAvailableForItems / participants.size + } LazyColumn( - modifier = Modifier.padding( - start = dimensions().spacing4x, - end = dimensions().spacing4x - ), + modifier = modifier, + contentPadding = PaddingValues(contentPadding), userScrollEnabled = false, - verticalArrangement = Arrangement.spacedBy(MaterialTheme.wireDimensions.spacing2x) + verticalArrangement = Arrangement.spacedBy(spacedBy) ) { items(items = participants, key = { it.id.toString() + it.clientId }) { participant -> // since we are getting participants by chunk of 8 items, @@ -71,11 +72,6 @@ fun CallingHorizontalView( val isSelfUser = remember(pageIndex, participants.first()) { pageIndex == 0 && participants.first() == participant } - val spacing4x = dimensions().spacing4x - val tileHeight = remember(participants.size) { - (contentHeight - spacing4x) / participants.size - } - ParticipantTile( modifier = Modifier .pointerInput(Unit) { @@ -93,7 +89,7 @@ fun CallingHorizontalView( } .fillMaxWidth() .height(tileHeight) - .animateItemPlacement(tween(durationMillis = 200)), + .animateItem(), participantTitleState = participant, isSelfUser = isSelfUser, isSelfUserMuted = isSelfUserMuted, @@ -105,41 +101,36 @@ fun CallingHorizontalView( } } +@Composable +fun PreviewCallingHorizontalView(participants: List, modifier: Modifier = Modifier) { + Box(modifier = modifier.height(800.dp)) { + CallingHorizontalView( + participants = participants, + pageIndex = 0, + isSelfUserMuted = true, + isSelfUserCameraOn = false, + contentHeight = 800.dp, + onSelfVideoPreviewCreated = {}, + onSelfClearVideoPreview = {}, + onDoubleTap = { } + ) + } +} + +@PreviewMultipleThemes +@Composable +fun PreviewCallingHorizontalView_1Participant() = WireTheme { + PreviewCallingHorizontalView(buildPreviewParticipantsList(1)) +} + +@PreviewMultipleThemes +@Composable +fun PreviewCallingHorizontalView_2Participants() = WireTheme { + PreviewCallingHorizontalView(buildPreviewParticipantsList(2)) +} + @PreviewMultipleThemes @Composable -fun PreviewCallingHorizontalView() { - val participant1 = UICallParticipant( - id = QualifiedID("", ""), - clientId = "client-id", - name = "user name", - isMuted = true, - isSpeaking = false, - isCameraOn = false, - isSharingScreen = false, - avatar = null, - membership = Membership.Admin, - hasEstablishedAudio = true - ) - val participant2 = UICallParticipant( - id = QualifiedID("", ""), - clientId = "client-id", - name = "user name 2", - isMuted = true, - isSpeaking = false, - isCameraOn = false, - isSharingScreen = false, - avatar = null, - membership = Membership.Admin, - hasEstablishedAudio = true - ) - CallingHorizontalView( - participants = listOf(participant1, participant2), - pageIndex = 0, - isSelfUserMuted = true, - isSelfUserCameraOn = false, - contentHeight = 500.dp, - onSelfVideoPreviewCreated = {}, - onSelfClearVideoPreview = {}, - onDoubleTap = { } - ) +fun PreviewCallingHorizontalView_3Participants() = WireTheme { + PreviewCallingHorizontalView(buildPreviewParticipantsList(3)) } diff --git a/app/src/main/kotlin/com/wire/android/ui/common/UserProfileAvatar.kt b/app/src/main/kotlin/com/wire/android/ui/common/UserProfileAvatar.kt index 3563433810c..0cee86784c6 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/UserProfileAvatar.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/UserProfileAvatar.kt @@ -52,17 +52,28 @@ import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.UserAvailabilityStatus import kotlin.math.sqrt +/** + * @param avatarData data for the avatar + * @param modifier modifier for the avatar composable + * @param size size of the inner avatar itself, without any padding or indicators borders, if [UserProfileAvatarType.WithIndicators] then + * composable will be larger than this specified size by the indicators borders widths, if padding is specified it will also be added to + * the final composable size + * @param padding padding around the avatar and indicator borders + * @param clickable clickable callback for the avatar + * @param showPlaceholderIfNoAsset if true, will show default avatar if asset is null + * @param withCrossfadeAnimation if true, will animate the avatar change + * @param type type of the avatar, if [UserProfileAvatarType.WithIndicators] then composable will be larger by the indicators borders + */ @Composable fun UserProfileAvatar( - avatarData: UserAvatarData = UserAvatarData(), + avatarData: UserAvatarData, + modifier: Modifier = Modifier, size: Dp = MaterialTheme.wireDimensions.avatarDefaultSize, padding: Dp = MaterialTheme.wireDimensions.avatarClickablePadding, - modifier: Modifier = Modifier, clickable: Clickable? = null, showPlaceholderIfNoAsset: Boolean = true, withCrossfadeAnimation: Boolean = false, - showStatusIndicator: Boolean = true, - withLegalHoldIndicator: Boolean = false, + type: UserProfileAvatarType = UserProfileAvatarType.WithIndicators(legalHoldIndicatorVisible = false), ) { Box( contentAlignment = Alignment.Center, @@ -80,40 +91,52 @@ fun UserProfileAvatar( painter = painter, contentDescription = stringResource(R.string.content_description_user_avatar), modifier = Modifier - // we need to take borders into account - .size(size + (max(dimensions().avatarStatusBorderSize, dimensions().avatarLegalHoldIndicatorBorderSize) * 2)) - .let { - if (withLegalHoldIndicator) { - it - .border( - width = dimensions().avatarLegalHoldIndicatorBorderSize / 2, - shape = CircleShape, - color = colorsScheme().error.copy(alpha = 0.3f) - ) - .padding(dimensions().avatarLegalHoldIndicatorBorderSize / 2) - .border( - width = dimensions().avatarLegalHoldIndicatorBorderSize / 2, - shape = CircleShape, - color = colorsScheme().error.copy(alpha = 1.0f) - ) - .padding(dimensions().avatarLegalHoldIndicatorBorderSize / 2) - } else { - it - // this is to make the border of the avatar to be the same size as with the legal hold indicator - .padding(dimensions().avatarLegalHoldIndicatorBorderSize - dimensions().spacing1x) - .border( - width = dimensions().spacing1x, - shape = CircleShape, - color = colorsScheme().outline - ) - .padding(dimensions().spacing1x) + .size( + when (type) { + is UserProfileAvatarType.WithIndicators -> { + // indicator borders need to be taken into account, the avatar itself will be smaller by the borders widths + size + (max(dimensions().avatarStatusBorderSize, dimensions().avatarLegalHoldIndicatorBorderSize) * 2) + } + UserProfileAvatarType.WithoutIndicators -> { + // indicator borders don't need to be taken into account, the avatar itself will take all available space + size + } } + ) + .let { + if (type is UserProfileAvatarType.WithIndicators) { + if (type.legalHoldIndicatorVisible) { + it + .border( + width = dimensions().avatarLegalHoldIndicatorBorderSize / 2, + shape = CircleShape, + color = colorsScheme().error.copy(alpha = 0.3f) + ) + .padding(dimensions().avatarLegalHoldIndicatorBorderSize / 2) + .border( + width = dimensions().avatarLegalHoldIndicatorBorderSize / 2, + shape = CircleShape, + color = colorsScheme().error.copy(alpha = 1.0f) + ) + .padding(dimensions().avatarLegalHoldIndicatorBorderSize / 2) + } else { + it + // this is to make the border of the avatar to be the same size as with the legal hold indicator + .padding(dimensions().avatarLegalHoldIndicatorBorderSize - dimensions().spacing1x) + .border( + width = dimensions().spacing1x, + shape = CircleShape, + color = colorsScheme().outline + ) + .padding(dimensions().spacing1x) + } + } else it } .clip(CircleShape) .testTag("User avatar"), contentScale = ContentScale.Crop ) - if (showStatusIndicator) { + if (type is UserProfileAvatarType.WithIndicators) { val avatarWithLegalHoldRadius = (size.value / 2f) + dimensions().avatarLegalHoldIndicatorBorderSize.value val statusRadius = (dimensions().userAvatarStatusSize - dimensions().avatarStatusBorderSize).value / 2f // calculated using the trigonometry so that the status is always in the right place according to the avatar @@ -129,6 +152,15 @@ fun UserProfileAvatar( } } +sealed class UserProfileAvatarType { + + // this will take the indicators into account when calculating avatar size so the composable itself will be larger by the borders + data class WithIndicators(val legalHoldIndicatorVisible: Boolean) : UserProfileAvatarType() + + // this will not take the indicators into account when calculating avatar size so the avatar itself will be exactly as specified size + data object WithoutIndicators : UserProfileAvatarType() +} + /** * Workaround to have profile avatar available for preview * @see [painter] https://developer.android.com/jetpack/compose/tooling @@ -173,7 +205,9 @@ private fun getDefaultAvatarResourceId(membership: Membership): Int = @Composable fun PreviewUserProfileAvatar() { WireTheme { - UserProfileAvatar(UserAvatarData(availabilityStatus = UserAvailabilityStatus.AVAILABLE)) + UserProfileAvatar( + avatarData = UserAvatarData(availabilityStatus = UserAvailabilityStatus.AVAILABLE), + ) } } @@ -181,7 +215,10 @@ fun PreviewUserProfileAvatar() { @Composable fun PreviewUserProfileAvatarWithLegalHold() { WireTheme { - UserProfileAvatar(UserAvatarData(availabilityStatus = UserAvailabilityStatus.AVAILABLE), withLegalHoldIndicator = true) + UserProfileAvatar( + avatarData = UserAvatarData(availabilityStatus = UserAvailabilityStatus.AVAILABLE), + type = UserProfileAvatarType.WithIndicators(legalHoldIndicatorVisible = true) + ) } } @@ -189,6 +226,23 @@ fun PreviewUserProfileAvatarWithLegalHold() { @Composable fun PreviewLargeUserProfileAvatarWithLegalHold() { WireTheme { - UserProfileAvatar(UserAvatarData(availabilityStatus = UserAvailabilityStatus.AVAILABLE), 48.dp, withLegalHoldIndicator = true) + UserProfileAvatar( + avatarData = UserAvatarData(availabilityStatus = UserAvailabilityStatus.AVAILABLE), + size = 48.dp, + type = UserProfileAvatarType.WithIndicators(legalHoldIndicatorVisible = true) + ) + } +} + +@PreviewMultipleThemes +@Composable +fun PreviewUserProfileAvatarWithoutIndicators() { + WireTheme { + UserProfileAvatar( + avatarData = UserAvatarData(), + padding = 0.dp, + size = 48.dp, + type = UserProfileAvatarType.WithoutIndicators, + ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/HomeTopBar.kt b/app/src/main/kotlin/com/wire/android/ui/home/HomeTopBar.kt index 29e748409c5..17527ee843e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/HomeTopBar.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/HomeTopBar.kt @@ -26,6 +26,7 @@ import com.wire.android.model.Clickable import com.wire.android.model.ImageAsset.UserAvatarAsset import com.wire.android.model.UserAvatarData import com.wire.android.ui.common.UserProfileAvatar +import com.wire.android.ui.common.UserProfileAvatarType import com.wire.android.ui.common.topappbar.NavigationIconType import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar import com.wire.android.ui.theme.WireTheme @@ -50,7 +51,7 @@ fun HomeTopBar( UserProfileAvatar( avatarData = UserAvatarData(avatarAsset, status), clickable = remember { Clickable(enabled = true) { onNavigateToSelfUserProfile() } }, - withLegalHoldIndicator = withLegalHoldIndicator, + type = UserProfileAvatarType.WithIndicators(legalHoldIndicatorVisible = withLegalHoldIndicator), ) }, elevation = elevation, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/UsersTypingIndicator.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/UsersTypingIndicator.kt index f2a9b4f71ba..cda320d0240 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/UsersTypingIndicator.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/UsersTypingIndicator.kt @@ -47,14 +47,17 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.times import com.wire.android.R import com.wire.android.di.hiltViewModelScoped import com.wire.android.model.UserAvatarData import com.wire.android.ui.common.UserProfileAvatar +import com.wire.android.ui.common.UserProfileAvatarType import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.common.dimensions import com.wire.android.ui.home.conversations.details.participants.model.UIParticipant @@ -82,11 +85,11 @@ fun UsersTypingIndicatorForConversation( } @Composable -fun UsersTypingIndicator(usersTyping: List) { +fun UsersTypingIndicator(usersTyping: List, modifier: Modifier = Modifier) { if (usersTyping.isNotEmpty()) { Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier + modifier = modifier .padding(bottom = dimensions().spacing4x) .height(dimensions().typingIndicatorHeight) .background( @@ -96,7 +99,10 @@ fun UsersTypingIndicator(usersTyping: List) { ) { val rememberTransition = rememberInfiniteTransition(label = stringResource(R.string.animation_label_typing_indicator_horizontal_transition)) - UsersTypingAvatarPreviews(usersTyping) + UsersTypingAvatarPreviews( + usersTyping = usersTyping, + modifier = Modifier.padding(horizontal = dimensions().spacing4x) + ) Text( text = pluralStringResource( R.plurals.typing_indicator_event_message, @@ -120,16 +126,32 @@ fun UsersTypingIndicator(usersTyping: List) { } } -@Suppress("MagicNumber") @Composable -private fun UsersTypingAvatarPreviews(usersTyping: List, maxPreviewsDisplay: Int = MAX_PREVIEWS_DISPLAY) { - Row(horizontalArrangement = Arrangement.spacedBy((-14).dp)) { +private fun UsersTypingAvatarPreviews( + usersTyping: List, + modifier: Modifier = Modifier, + maxPreviewsDisplay: Int = MAX_PREVIEWS_DISPLAY +) { + val avatarSize = dimensions().spacing16x + val borderWidth = dimensions().spacing1x + val avatarWithBorderSize = avatarSize + 2 * borderWidth + val roundedCornersSize = avatarWithBorderSize / 2 + val spacedBy = -(avatarSize / 2) + + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(spacedBy) + ) { usersTyping.take(maxPreviewsDisplay).forEach { user -> UserProfileAvatar( avatarData = user.avatarData, size = dimensions().spacing16x, - padding = dimensions().spacing2x, - showStatusIndicator = false, + modifier = Modifier + .clip(RoundedCornerShape(roundedCornersSize)) + .size(avatarWithBorderSize) + .background(colorsScheme().surfaceVariant), + padding = dimensions().spacing0x, + type = UserProfileAvatarType.WithoutIndicators, ) } } diff --git a/app/src/main/kotlin/com/wire/android/util/extension/LazyListScope.kt b/app/src/main/kotlin/com/wire/android/util/extension/LazyListScope.kt index 4f544a0bd0b..987eb752c39 100644 --- a/app/src/main/kotlin/com/wire/android/util/extension/LazyListScope.kt +++ b/app/src/main/kotlin/com/wire/android/util/extension/LazyListScope.kt @@ -18,7 +18,6 @@ package com.wire.android.util.extension -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.wrapContentSize @@ -28,7 +27,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.wire.android.ui.home.conversationslist.common.FolderHeader -@OptIn(ExperimentalFoundationApi::class) inline fun LazyListScope.folderWithElements( header: String? = null, items: Map, @@ -45,7 +43,7 @@ inline fun LazyListScope.folderWithElements( name = header, modifier = Modifier .fillMaxWidth() - .let { if (animateItemPlacement) it.animateItemPlacement() else it } + .let { if (animateItemPlacement) it.animateItem() else it } ) } } @@ -56,7 +54,7 @@ inline fun LazyListScope.folderWithElements( Box( modifier = Modifier .wrapContentSize() - .let { if (animateItemPlacement) it.animateItemPlacement() else it } + .let { if (animateItemPlacement) it.animateItem() else it } ) { factory(item.value) if (index <= list.lastIndex) { diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireColorScheme.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireColorScheme.kt index 6977a7ba6c8..4044d76754f 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireColorScheme.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireColorScheme.kt @@ -73,9 +73,12 @@ data class WireColorScheme( val disabledIndeterminateColor: Color, val disabledUncheckedColor: Color, val messageErrorBackgroundColor: Color, - val muteButtonColor: Color, val groupAvatarColors: List, val callingParticipantTileBackgroundColor: Color, + val callingParticipantNameBackground: Color, + val callingParticipantNameText: Color, + val callingParticipantNameConnectingText: Color, + val callingParticipantError: Color, val callingPagerIndicatorBackground: Color, val callingActiveIndicator: Color, val callingInActiveIndicator: Color, @@ -182,7 +185,6 @@ private val LightWireColorScheme = WireColorScheme( disabledIndeterminateColor = WireColorPalette.Gray80, disabledUncheckedColor = WireColorPalette.Gray80, messageErrorBackgroundColor = WireColorPalette.DarkRed50, - muteButtonColor = WireColorPalette.DarkRed500, groupAvatarColors = listOf( // Red WireColorPalette.LightRed300, @@ -214,6 +216,10 @@ private val LightWireColorScheme = WireColorScheme( WireColorPalette.Gray70, ), callingParticipantTileBackgroundColor = WireColorPalette.Gray90, + callingParticipantNameBackground = Color.Black, + callingParticipantNameText = Color.White, + callingParticipantNameConnectingText = WireColorPalette.Gray60, + callingParticipantError = WireColorPalette.DarkRed500, callingPagerIndicatorBackground = WireColorPalette.Gray40, callingActiveIndicator = WireColorPalette.LightBlue500, callingInActiveIndicator = Color.White, @@ -323,7 +329,6 @@ private val DarkWireColorScheme = WireColorScheme( disabledIndeterminateColor = WireColorPalette.Gray80, disabledUncheckedColor = WireColorPalette.Gray80, messageErrorBackgroundColor = WireColorPalette.GrayRed900, - muteButtonColor = WireColorPalette.DarkRed500, groupAvatarColors = listOf( // Red WireColorPalette.DarkRed300, @@ -354,7 +359,11 @@ private val DarkWireColorScheme = WireColorScheme( WireColorPalette.Gray70, WireColorPalette.Gray90, ), - callingParticipantTileBackgroundColor = WireColorPalette.Gray95, + callingParticipantTileBackgroundColor = WireColorPalette.Gray90, + callingParticipantNameBackground = Color.Black, + callingParticipantNameText = Color.White, + callingParticipantNameConnectingText = WireColorPalette.Gray60, + callingParticipantError = WireColorPalette.DarkRed500, callingPagerIndicatorBackground = WireColorPalette.Gray40, callingActiveIndicator = WireColorPalette.LightBlue500, callingInActiveIndicator = Color.White, diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireDimensions.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireDimensions.kt index 48fd8a2ecb5..26c55cfd629 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireDimensions.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireDimensions.kt @@ -132,9 +132,11 @@ data class WireDimensions( val spacing0x: Dp, val spacing1x: Dp, val spacing2x: Dp, + val spacing3x: Dp, val spacing4x: Dp, val spacing6x: Dp, val spacing8x: Dp, + val spacing10x: Dp, val spacing12x: Dp, val spacing16x: Dp, val spacing18x: Dp, @@ -153,9 +155,11 @@ data class WireDimensions( val spacing200x: Dp, // Corners val corner2x: Dp, + val corner3x: Dp, val corner4x: Dp, val corner6x: Dp, val corner8x: Dp, + val corner9x: Dp, val corner10x: Dp, val corner12x: Dp, val corner14x: Dp, @@ -182,7 +186,6 @@ data class WireDimensions( val defaultSheetPeekHeight: Dp, val defaultOutgoingCallSheetPeekHeight: Dp, val onGoingCallUserAvatarSize: Dp, - val onGoingCallUserAvatarMinimizedSize: Dp, val onGoingCallTileUsernameMaxWidth: Dp, val outgoingCallUserAvatarSize: Dp, val defaultIncomingCallSheetPeekHeight: Dp, @@ -286,9 +289,11 @@ private val DefaultPhonePortraitWireDimensions: WireDimensions = WireDimensions( spacing0x = 0.dp, spacing1x = 1.dp, spacing2x = 2.dp, + spacing3x = 3.dp, spacing4x = 4.dp, spacing6x = 6.dp, spacing8x = 8.dp, + spacing10x = 10.dp, spacing12x = 12.dp, spacing16x = 16.dp, spacing18x = 18.dp, @@ -306,9 +311,11 @@ private val DefaultPhonePortraitWireDimensions: WireDimensions = WireDimensions( spacing120x = 120.dp, spacing200x = 200.dp, corner2x = 2.dp, + corner3x = 3.dp, corner4x = 4.dp, corner6x = 6.dp, corner8x = 8.dp, + corner9x = 9.dp, corner10x = 10.dp, corner12x = 12.dp, corner14x = 14.dp, @@ -332,8 +339,7 @@ private val DefaultPhonePortraitWireDimensions: WireDimensions = WireDimensions( bigCallingAcceptButtonIconSize = 24.dp, defaultSheetPeekHeight = 100.dp, defaultOutgoingCallSheetPeekHeight = 281.dp, - onGoingCallUserAvatarSize = 90.dp, - onGoingCallUserAvatarMinimizedSize = 60.dp, + onGoingCallUserAvatarSize = 72.dp, onGoingCallTileUsernameMaxWidth = 120.dp, outgoingCallUserAvatarSize = 128.dp, defaultIncomingCallSheetPeekHeight = 280.dp,