Merge pull request #6574 from element-hq/feature/valere/call/ongoing_voice_call_join

feat: Default to camera muted when joining ongoing voice call
This commit is contained in:
Valere Fedronic 2026-04-14 22:47:58 +02:00 committed by GitHub
commit 8bd6035511
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 138 additions and 3 deletions

View file

@ -60,6 +60,7 @@ import io.element.android.libraries.designsystem.theme.roomListRoomMessage
import io.element.android.libraries.designsystem.theme.roomListRoomMessageDate
import io.element.android.libraries.designsystem.theme.roomListRoomName
import io.element.android.libraries.designsystem.theme.unreadIndicator
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.ui.components.InviteSenderView
import io.element.android.libraries.matrix.ui.model.InviteSender
@ -349,6 +350,7 @@ private fun MessagePreviewAndIndicatorRow(
if (room.hasRoomCall) {
OnGoingCallIcon(
color = tint,
isAudio = room.activeCallIntent == CallIntent.AUDIO
)
}
if (room.userDefinedNotificationMode == RoomNotificationMode.MUTE) {
@ -398,10 +400,11 @@ private fun InviteNameAndIndicatorRow(
@Composable
private fun OnGoingCallIcon(
color: Color,
isAudio: Boolean
) {
Icon(
modifier = Modifier.size(16.dp),
imageVector = CompoundIcons.VideoCallSolid(),
imageVector = if (isAudio) CompoundIcons.VoiceCallSolid() else CompoundIcons.VideoCallSolid(),
contentDescription = stringResource(CommonStrings.a11y_notifications_ongoing_call),
tint = color,
)

View file

@ -17,6 +17,7 @@ import io.element.android.libraries.dateformatter.api.DateFormatter
import io.element.android.libraries.dateformatter.api.DateFormatterMode
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.eventformatter.api.RoomLatestEventFormatter
import io.element.android.libraries.matrix.api.room.CallIntentConsensus
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.roomlist.LatestEventValue
@ -50,6 +51,11 @@ class RoomListRoomSummaryFactory(
avatarData = avatarData,
userDefinedNotificationMode = roomInfo.userDefinedNotificationMode,
hasRoomCall = roomInfo.hasRoomCall,
activeCallIntent = when (val consensus = roomInfo.activeCallIntentConsensus) {
is CallIntentConsensus.Full -> consensus.callIntent
is CallIntentConsensus.Partial -> consensus.callIntent
CallIntentConsensus.None -> null
},
isDirect = roomInfo.isDirect,
isFavorite = roomInfo.isFavorite,
inviteSender = roomInfo.inviter?.toInviteSender(),

View file

@ -13,6 +13,7 @@ import io.element.android.features.invite.api.InviteData
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.ui.model.InviteSender
import kotlinx.collections.immutable.ImmutableList
@ -33,6 +34,7 @@ data class RoomListRoomSummary(
val avatarData: AvatarData,
val userDefinedNotificationMode: RoomNotificationMode?,
val hasRoomCall: Boolean,
val activeCallIntent: CallIntent?,
val isDirect: Boolean,
val isDm: Boolean,
val isFavorite: Boolean,

View file

@ -14,6 +14,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.ui.model.InviteSender
import kotlinx.collections.immutable.toImmutableList
@ -132,6 +133,14 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSu
listOf(
aRoomListRoomSummary(latestEvent = LatestEvent.Sending("A sending message")),
aRoomListRoomSummary(latestEvent = LatestEvent.Error),
),
listOf(
aRoomListRoomSummary(
name = "Active voice call",
latestEvent = LatestEvent.Synced("No activity, call"),
hasRoomCall = true,
activeCallIntent = CallIntent.AUDIO
),
)
).flatten()
}
@ -158,6 +167,7 @@ internal fun aRoomListRoomSummary(
timestamp: String? = latestEvent.takeIf { it !is LatestEvent.None }?.let { "88:88" },
notificationMode: RoomNotificationMode? = null,
hasRoomCall: Boolean = false,
activeCallIntent: CallIntent? = null,
avatarData: AvatarData = AvatarData(id, name, size = AvatarSize.RoomListItem),
isDirect: Boolean = false,
isDm: Boolean = false,
@ -181,6 +191,7 @@ internal fun aRoomListRoomSummary(
avatarData = avatarData,
userDefinedNotificationMode = notificationMode,
hasRoomCall = hasRoomCall,
activeCallIntent = activeCallIntent,
isDirect = isDirect,
isDm = isDm,
isFavorite = isFavorite,

View file

@ -101,6 +101,7 @@ internal fun createRoomListRoomSummary(
displayType = displayType,
userDefinedNotificationMode = userDefinedNotificationMode,
hasRoomCall = false,
activeCallIntent = null,
isDirect = false,
isFavorite = isFavorite,
canonicalAlias = null,

View file

@ -20,6 +20,8 @@ import io.element.android.features.call.api.CurrentCallService
import io.element.android.features.enterprise.api.SessionEnterpriseService
import io.element.android.features.roomcall.api.RoomCallState
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.room.CallIntentConsensus
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.room.powerlevels.canCall
@ -57,8 +59,7 @@ class RoomCallStatePresenter(
canJoinCall = canJoinCall,
isUserInTheCall = isUserInTheCall,
isUserLocallyInTheCall = isUserLocallyInTheCall,
// TODO resolve intent while the call is ongoing
isAudioCall = false
isAudioCall = roomInfo.activeCallIntentConsensus.isAudio(),
)
else -> RoomCallState.StandBy(
canStartCall = canJoinCall,
@ -70,3 +71,12 @@ class RoomCallStatePresenter(
return callState
}
}
fun CallIntentConsensus.isAudio(): Boolean {
val intent = when (this) {
is CallIntentConsensus.Full -> callIntent
is CallIntentConsensus.Partial -> callIntent
is CallIntentConsensus.None -> return false
}
return intent == CallIntent.AUDIO
}

View file

@ -14,6 +14,8 @@ import io.element.android.features.call.api.CurrentCallService
import io.element.android.features.call.test.FakeCurrentCallService
import io.element.android.features.enterprise.test.FakeSessionEnterpriseService
import io.element.android.features.roomcall.api.RoomCallState
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.room.CallIntentConsensus
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
@ -188,6 +190,100 @@ class RoomCallStatePresenterTest {
}
}
@Test
fun `present - active call with audio Intent`() = runTest {
val room = FakeJoinedRoom(
baseRoom = FakeBaseRoom(
roomPermissions = roomPermissions(true),
).apply {
givenRoomInfo(
aRoomInfo(
hasRoomCall = true,
activeCallIntentConsensus = CallIntentConsensus.Full(CallIntent.AUDIO),
activeRoomCallParticipants = emptyList(),
)
)
}
)
val presenter = createRoomCallStatePresenter(
joinedRoom = room,
currentCallService = FakeCurrentCallService(MutableStateFlow(CurrentCall.RoomCall(room.roomId))),
)
presenter.test {
skipItems(1)
assertThat(awaitItem()).isEqualTo(
RoomCallState.OnGoing(
canJoinCall = true,
isAudioCall = true,
isUserInTheCall = false,
isUserLocallyInTheCall = true,
)
)
}
}
@Test
fun `present - active call with partial audio Intent`() = runTest {
val room = FakeJoinedRoom(
baseRoom = FakeBaseRoom(
roomPermissions = roomPermissions(true),
).apply {
givenRoomInfo(
aRoomInfo(
hasRoomCall = true,
activeCallIntentConsensus = CallIntentConsensus.Partial(CallIntent.AUDIO, 1, 4),
)
)
}
)
val presenter = createRoomCallStatePresenter(
joinedRoom = room,
currentCallService = FakeCurrentCallService(MutableStateFlow(CurrentCall.RoomCall(room.roomId))),
)
presenter.test {
skipItems(1)
assertThat(awaitItem()).isEqualTo(
RoomCallState.OnGoing(
canJoinCall = true,
isAudioCall = true,
isUserInTheCall = false,
isUserLocallyInTheCall = true,
)
)
}
}
@Test
fun `present - active call with no intent defaults to Audio`() = runTest {
val room = FakeJoinedRoom(
baseRoom = FakeBaseRoom(
roomPermissions = roomPermissions(true),
).apply {
givenRoomInfo(
aRoomInfo(
hasRoomCall = true,
activeCallIntentConsensus = CallIntentConsensus.None,
)
)
}
)
val presenter = createRoomCallStatePresenter(
joinedRoom = room,
currentCallService = FakeCurrentCallService(MutableStateFlow(CurrentCall.RoomCall(room.roomId))),
)
presenter.test {
skipItems(1)
assertThat(awaitItem()).isEqualTo(
RoomCallState.OnGoing(
canJoinCall = true,
isAudioCall = false,
isUserInTheCall = false,
isUserLocallyInTheCall = true,
)
)
}
}
@Test
fun `present - user leaves the call`() = runTest {
val room = FakeJoinedRoom(