Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: show connecting indicator if user has bad/lost connection during a call (WPB-1125) #2101

Merged
merged 10 commits into from
Aug 25, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we write some test for it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's already tested in UICallParticipantMapperTest.kt

)
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,5 @@ data class UICallParticipant(
val isSharingScreen: Boolean,
val avatar: ImageAsset.UserAvatarAsset? = null,
val membership: Membership,
val hasEstablishedAudio: Boolean
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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) }
Expand All @@ -106,6 +110,7 @@ fun ParticipantTile(
AvatarTile(
modifier = Modifier
.fillMaxSize()
.alpha(alpha)
.constrainAs(avatar) { },
avatar = UserAvatarData(participantTitleState.avatar),
avatarSize = avatarSize
Expand Down Expand Up @@ -178,7 +183,8 @@ fun ParticipantTile(
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)
},
isMuted = participantTitleState.isMuted
isMuted = participantTitleState.isMuted,
hasEstablishedAudio = participantTitleState.hasEstablishedAudio
)

UsernameTile(
Expand All @@ -191,7 +197,8 @@ fun ParticipantTile(
}
.widthIn(max = onGoingCallTileUsernameMaxWidth),
name = participantTitleState.name,
isSpeaking = participantTitleState.isSpeaking
isSpeaking = participantTitleState.isSpeaking,
hasEstablishedAudio = participantTitleState.hasEstablishedAudio
)
}
TileBorder(participantTitleState.isSpeaking)
Expand Down Expand Up @@ -277,32 +284,74 @@ 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: maybe we should move all these hard-coded to the theme colors as well, they would just be the same for both light and dark mode, but it would ensure we have all colors in one place so if we decide to change one of them we wouldn't need to look for them in the whole project

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, not that simple by just changing the color in the file.. we need to check the whole project in all cases I would say especially for white and black


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(
saleniuk marked this conversation as resolved.
Show resolved Hide resolved
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,
)
}
}
}
}

@Composable
private fun MicrophoneTile(
modifier: Modifier,
isMuted: Boolean,
hasEstablishedAudio: Boolean
) {
if (isMuted) {
if (isMuted && hasEstablishedAudio) {
Surface(
modifier = modifier,
color = Color.Black,
Expand All @@ -319,21 +368,70 @@ private fun MicrophoneTile(
}
}

@Preview
@Preview("Default view")
@Composable
fun PreviewParticipantTile() {
ParticipantTile(
modifier = Modifier.height(300.dp),
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 = {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,20 +110,18 @@ 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

ParticipantTile(
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)
Expand Down Expand Up @@ -177,6 +175,7 @@ fun PreviewGroupCallGrid() {
isSharingScreen = false,
avatar = null,
membership = Membership.Admin,
hasEstablishedAudio = true
),
UICallParticipant(
id = QualifiedID("", ""),
Expand All @@ -188,6 +187,7 @@ fun PreviewGroupCallGrid() {
isSharingScreen = false,
avatar = null,
membership = Membership.Admin,
hasEstablishedAudio = true
)
),
contentHeight = 800.dp,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1150,7 +1150,7 @@

<!-- Record Audio -->
<string name="record_audio_start_label">Start Recording</string>
<string name="record_audio_recording_label">Recording Audio...</string>
<string name="record_audio_recording_label">Recording Audio</string>
<string name="record_audio_send_label">Send Audio Message</string>
<string name="record_audio_discard_dialog_title">Discard Audio Message?</string>
<string name="record_audio_discard_dialog_text">The audio message will be deleted and can not be sent.</string>
Expand All @@ -1165,4 +1165,5 @@
<string name="last_message_composite_with_missing_text">sent an interactive message</string>
<string name="join_conversation_dialog_password_label">Conversation Password</string>
<string name="join_conversation_dialog_password_placeholder">Enter password</string>
<string name="participant_tile_call_connecting_label">Connecting…</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -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"),
Expand All @@ -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)
}
Expand Down
Loading