diff --git a/app/src/main/kotlin/com/wire/android/mapper/UICallParticipantMapper.kt b/app/src/main/kotlin/com/wire/android/mapper/UICallParticipantMapper.kt index a638adc6c0..6dc244c97e 100644 --- a/app/src/main/kotlin/com/wire/android/mapper/UICallParticipantMapper.kt +++ b/app/src/main/kotlin/com/wire/android/mapper/UICallParticipantMapper.kt @@ -39,6 +39,7 @@ class UICallParticipantMapper @Inject constructor( isCameraOn = participant.isCameraOn, isSharingScreen = participant.isSharingScreen, avatar = participant.avatarAssetId?.let { ImageAsset.UserAvatarAsset(wireSessionImageLoader, it) }, - membership = userTypeMapper.toMembership(participant.userType) + membership = userTypeMapper.toMembership(participant.userType), + hasEstablishedAudio = participant.hasEstablishedAudio ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/model/UICallParticipant.kt b/app/src/main/kotlin/com/wire/android/ui/calling/model/UICallParticipant.kt index 2162bd5d89..4d13b6ac6b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/model/UICallParticipant.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/model/UICallParticipant.kt @@ -34,4 +34,5 @@ data class UICallParticipant( val isSharingScreen: Boolean, val avatar: ImageAsset.UserAvatarAsset? = null, val membership: Membership, + val hasEstablishedAudio: Boolean ) 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 6285ba936a..faaa59128e 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 @@ -30,12 +30,14 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Surface -import androidx.compose.material3.Text +import androidx.compose.material.ContentAlpha import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue @@ -44,6 +46,7 @@ import androidx.compose.runtime.remember 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 @@ -90,10 +93,11 @@ fun ParticipantTile( onSelfUserVideoPreviewCreated: (view: View) -> Unit, onClearSelfUserVideoPreview: () -> Unit ) { + val alpha = if (participantTitleState.hasEstablishedAudio) ContentAlpha.high else ContentAlpha.medium Surface( modifier = modifier, color = colorsScheme().callingParticipantTileBackgroundColor, - shape = RoundedCornerShape(dimensions().corner6x) + shape = RoundedCornerShape(dimensions().corner6x), ) { var size by remember { mutableStateOf(IntSize.Zero) } var zoom by remember { mutableStateOf(1f) } @@ -106,6 +110,7 @@ fun ParticipantTile( AvatarTile( modifier = Modifier .fillMaxSize() + .alpha(alpha) .constrainAs(avatar) { }, avatar = UserAvatarData(participantTitleState.avatar), avatarSize = avatarSize @@ -178,7 +183,8 @@ fun ParticipantTile( bottom.linkTo(parent.bottom) start.linkTo(parent.start) }, - isMuted = participantTitleState.isMuted + isMuted = participantTitleState.isMuted, + hasEstablishedAudio = participantTitleState.hasEstablishedAudio ) UsernameTile( @@ -191,7 +197,8 @@ fun ParticipantTile( } .widthIn(max = onGoingCallTileUsernameMaxWidth), name = participantTitleState.name, - isSpeaking = participantTitleState.isSpeaking + isSpeaking = participantTitleState.isSpeaking, + hasEstablishedAudio = participantTitleState.hasEstablishedAudio ) } TileBorder(participantTitleState.isSpeaking) @@ -277,23 +284,64 @@ private fun AvatarTile( private fun UsernameTile( modifier: Modifier, name: String, - isSpeaking: Boolean + isSpeaking: Boolean, + hasEstablishedAudio: Boolean ) { val color = if (isSpeaking) MaterialTheme.wireColorScheme.primary else Color.Black + val nameLabelColor = if (hasEstablishedAudio) Color.White else colorsScheme().secondaryText - Surface( - modifier = modifier, - shape = RoundedCornerShape(dimensions().corner4x), - color = color - ) { - Text( - color = Color.White, - style = MaterialTheme.wireTypography.label01, - modifier = Modifier.padding(dimensions().spacing4x), - text = name, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) + ConstraintLayout(modifier = modifier) { + val (nameLabel, connectingLabel) = createRefs() + + 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 + ) { + Text( + color = nameLabelColor, + style = MaterialTheme.wireTypography.label01, + modifier = Modifier.padding(dimensions().spacing4x), + text = name, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + 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 + ) { + Text( + color = colorsScheme().error, + 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, + ) + } + } } } @@ -301,8 +349,9 @@ private fun UsernameTile( private fun MicrophoneTile( modifier: Modifier, isMuted: Boolean, + hasEstablishedAudio: Boolean ) { - if (isMuted) { + if (isMuted && hasEstablishedAudio) { Surface( modifier = modifier, color = Color.Black, @@ -319,7 +368,7 @@ private fun MicrophoneTile( } } -@Preview +@Preview("Default view") @Composable fun PreviewParticipantTile() { ParticipantTile( @@ -327,13 +376,62 @@ fun PreviewParticipantTile() { participantTitleState = UICallParticipant( id = QualifiedID("", ""), clientId = "client-id", - name = "name", + name = "user name", isMuted = true, + isSpeaking = false, + isCameraOn = false, + isSharingScreen = false, + avatar = null, + membership = Membership.Admin, + hasEstablishedAudio = true + ), + onClearSelfUserVideoPreview = {}, + onSelfUserVideoPreviewCreated = {}, + isSelfUser = false + ) +} + +@Preview +@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 = true, + isCameraOn = false, + isSharingScreen = false, + avatar = null, + membership = Membership.Admin, + hasEstablishedAudio = true + ), + onClearSelfUserVideoPreview = {}, + onSelfUserVideoPreviewCreated = {}, + isSelfUser = false + ) +} + +@Preview +@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 + membership = Membership.Admin, + hasEstablishedAudio = false ), onClearSelfUserVideoPreview = {}, onSelfUserVideoPreviewCreated = {}, 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 828a2cfb11..79b1729fae 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 @@ -110,7 +110,8 @@ fun GroupCallGrid( isCameraOn = isCameraOn, isSharingScreen = participant.isSharingScreen, avatar = participant.avatar, - membership = participant.membership + membership = participant.membership, + hasEstablishedAudio = participant.hasEstablishedAudio ) val tileHeight = (contentHeight - dimensions().spacing4x) / numberOfTilesRows @@ -118,12 +119,9 @@ fun GroupCallGrid( modifier = Modifier .pointerInput(Unit) { detectTapGestures( - onPress = { /* Called when the gesture starts */ }, onDoubleTap = { onDoubleTap(participantState.id, participantState.clientId, isSelfUser) - }, - onLongPress = { /* Called on Long Press */ }, - onTap = { /* Called on Tap */ } + } ) } .height(tileHeight) @@ -177,6 +175,7 @@ fun PreviewGroupCallGrid() { isSharingScreen = false, avatar = null, membership = Membership.Admin, + hasEstablishedAudio = true ), UICallParticipant( id = QualifiedID("", ""), @@ -188,6 +187,7 @@ fun PreviewGroupCallGrid() { isSharingScreen = false, avatar = null, membership = Membership.Admin, + hasEstablishedAudio = true ) ), contentHeight = 800.dp, 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 e73d10bf9f..bb4fbd2dbe 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 @@ -94,7 +94,8 @@ fun CallingHorizontalView( isCameraOn = isCameraOn, isSharingScreen = participant.isSharingScreen, avatar = participant.avatar, - membership = participant.membership + membership = participant.membership, + hasEstablishedAudio = participant.hasEstablishedAudio ) val tileHeight = (contentHeight - dimensions().spacing4x) / participants.size @@ -103,12 +104,9 @@ fun CallingHorizontalView( modifier = Modifier .pointerInput(Unit) { detectTapGestures( - onPress = { /* Called when the gesture starts */ }, onDoubleTap = { onDoubleTap(participantState.id, participantState.clientId, isSelfUser) - }, - onLongPress = { /* Called on Long Press */ }, - onTap = { /* Called on Tap */ } + } ) } .fillMaxWidth() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ffe2b17a33..e4be118613 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1150,7 +1150,7 @@ Start Recording - Recording Audio... + Recording Audio… Send Audio Message Discard Audio Message? The audio message will be deleted and can not be sent. @@ -1165,6 +1165,7 @@ sent an interactive message Conversation Password Enter password + Connecting… Create Guest Link Create password secured link Create link without password diff --git a/app/src/test/kotlin/com/wire/android/mapper/UICallParticipantMapperTest.kt b/app/src/test/kotlin/com/wire/android/mapper/UICallParticipantMapperTest.kt index a38a35d94d..d5158a1d90 100644 --- a/app/src/test/kotlin/com/wire/android/mapper/UICallParticipantMapperTest.kt +++ b/app/src/test/kotlin/com/wire/android/mapper/UICallParticipantMapperTest.kt @@ -22,7 +22,6 @@ package com.wire.android.mapper import com.wire.kalium.logic.data.call.Participant import com.wire.kalium.logic.data.id.QualifiedID -import com.wire.kalium.logic.data.user.UserAssetId import io.mockk.MockKAnnotations import io.mockk.mockk import kotlinx.coroutines.test.runTest @@ -32,18 +31,17 @@ class UICallParticipantMapperTest { @Test fun givenParticipant_whenMappingToUICallParticipant_thenCorrectValuesShouldBeReturned() = runTest { - val (arrangement, mapper) = Arrangement().arrange() + val (_, mapper) = Arrangement().arrange() // Given val item = Participant( - QualifiedID("idvalue", "iddomain"), - "clientId", - "name", - false, - false, - false, - false, - true, - UserAssetId("assetvalue", "assetdomain") + id = QualifiedID("value", "domain"), + clientId = "clientId", + name = "name", + isMuted = false, + isCameraOn = false, + isSpeaking = false, + isSharingScreen = false, + hasEstablishedAudio = true, ) // When val result = mapper.toUICallParticipant(item) diff --git a/app/src/test/kotlin/com/wire/android/ui/calling/OngoingCallViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/calling/OngoingCallViewModelTest.kt index 87e12234ae..9a01dd7514 100644 --- a/app/src/test/kotlin/com/wire/android/ui/calling/OngoingCallViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/calling/OngoingCallViewModelTest.kt @@ -127,7 +127,8 @@ class OngoingCallViewModelTest { isSpeaking = false, isCameraOn = true, isSharingScreen = false, - membership = Membership.None + membership = Membership.None, + hasEstablishedAudio = true ) private val participant2 = UICallParticipant( id = QualifiedID("value2", "domain"), @@ -137,7 +138,8 @@ class OngoingCallViewModelTest { isSpeaking = false, isCameraOn = false, isSharingScreen = false, - membership = Membership.None + membership = Membership.None, + hasEstablishedAudio = true ) private val participant3 = UICallParticipant( id = QualifiedID("value3", "domain"), @@ -147,7 +149,8 @@ class OngoingCallViewModelTest { isSpeaking = false, isCameraOn = true, isSharingScreen = true, - membership = Membership.None + membership = Membership.None, + hasEstablishedAudio = true ) val participants = listOf(participant1, participant2, participant3) }