From 58e66963d8a183d2860daf95739c397047c8be85 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 5 Nov 2024 17:39:39 +0100 Subject: [PATCH 1/6] Hide the join call button if the user is already in the call. This is at the account level so if the user has joined the call on another device, the join button will be hidden. Extract room call state presenter to its own module and update RoomCallState model. Let RoomDetailsPresenter use the new RoomCallStatePresenter --- features/messages/impl/build.gradle.kts | 1 + .../messages/impl/MessagesPresenter.kt | 14 +- .../features/messages/impl/MessagesState.kt | 9 +- .../messages/impl/MessagesStateProvider.kt | 11 +- .../features/messages/impl/MessagesView.kt | 33 +---- .../list/PinnedMessagesListPresenter.kt | 4 +- .../impl/timeline/TimelinePresenter.kt | 8 +- .../messages/impl/timeline/TimelineState.kt | 3 +- .../impl/timeline/TimelineStateProvider.kt | 3 +- .../impl/timeline/components/CallMenuItem.kt | 120 ++++++++++++++++++ .../timeline/components/JoinCallMenuItem.kt | 52 -------- .../components/TimelineItemCallNotifyView.kt | 31 ++--- .../timeline/components/TimelineItemRow.kt | 2 +- .../messages/impl/MessagesPresenterTest.kt | 23 +--- features/roomcall/api/build.gradle.kts | 20 +++ .../features/roomcall/api/RoomCallState.kt | 29 +++++ .../roomcall/api/RoomCallStateProvider.kt | 34 +++++ features/roomcall/impl/build.gradle.kts | 36 ++++++ .../roomcall/impl/RoomCallStatePresenter.kt | 43 +++++++ .../roomcall/impl/di/RoomCallModule.kt | 23 ++++ .../impl/RoomCallStatePresenterTest.kt | 46 +++++++ features/roomdetails/impl/build.gradle.kts | 1 + .../roomdetails/impl/RoomDetailsPresenter.kt | 9 +- .../roomdetails/impl/RoomDetailsState.kt | 3 +- .../impl/RoomDetailsStateProvider.kt | 8 +- .../roomdetails/impl/RoomDetailsView.kt | 4 +- .../impl/RoomDetailsPresenterTest.kt | 2 + 27 files changed, 419 insertions(+), 153 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/CallMenuItem.kt delete mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/JoinCallMenuItem.kt create mode 100644 features/roomcall/api/build.gradle.kts create mode 100644 features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallState.kt create mode 100644 features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallStateProvider.kt create mode 100644 features/roomcall/impl/build.gradle.kts create mode 100644 features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt create mode 100644 features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/di/RoomCallModule.kt create mode 100644 features/roomcall/impl/src/test/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenterTest.kt diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index 892c69ea2c..824e8c1692 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -29,6 +29,7 @@ dependencies { implementation(projects.features.call.api) implementation(projects.features.location.api) implementation(projects.features.poll.api) + implementation(projects.features.roomcall.api) implementation(projects.libraries.androidutils) implementation(projects.libraries.core) implementation(projects.libraries.architecture) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index b215fc49fd..7840c307c7 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -50,6 +50,7 @@ import io.element.android.features.messages.impl.timeline.protection.TimelinePro import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkStatus +import io.element.android.features.roomcall.api.RoomCallState import io.element.android.libraries.androidutils.clipboard.ClipboardHelper import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter @@ -75,7 +76,6 @@ import io.element.android.libraries.matrix.api.room.powerlevels.canSendMessage import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId import io.element.android.libraries.matrix.ui.messages.reply.map import io.element.android.libraries.matrix.ui.model.getAvatarData -import io.element.android.libraries.matrix.ui.room.canCall import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.services.analytics.api.AnalyticsService @@ -98,6 +98,7 @@ class MessagesPresenter @AssistedInject constructor( private val reactionSummaryPresenter: Presenter, private val readReceiptBottomSheetPresenter: Presenter, private val pinnedMessagesBannerPresenter: Presenter, + private val roomCallStatePresenter: Presenter, private val networkMonitor: NetworkMonitor, private val snackbarDispatcher: SnackbarDispatcher, private val dispatchers: CoroutineDispatchers, @@ -133,6 +134,7 @@ class MessagesPresenter @AssistedInject constructor( val reactionSummaryState = reactionSummaryPresenter.present() val readReceiptBottomSheetState = readReceiptBottomSheetPresenter.present() val pinnedMessagesBannerState = pinnedMessagesBannerPresenter.present() + val roomCallState = roomCallStatePresenter.present() val syncUpdateFlow = room.syncUpdateFlow.collectAsState() @@ -152,8 +154,6 @@ class MessagesPresenter @AssistedInject constructor( mutableStateOf(false) } - val canJoinCall by room.canCall(updateKey = syncUpdateFlow.value) - LaunchedEffect(Unit) { // Remove the unread flag on entering but don't send read receipts // as those will be handled by the timeline. @@ -204,12 +204,6 @@ class MessagesPresenter @AssistedInject constructor( } } - val callState = when { - !canJoinCall -> RoomCallState.DISABLED - roomInfo?.hasRoomCall == true -> RoomCallState.ONGOING - else -> RoomCallState.ENABLED - } - return MessagesState( roomId = room.roomId, roomName = roomName, @@ -232,7 +226,7 @@ class MessagesPresenter @AssistedInject constructor( enableTextFormatting = MessageComposerConfig.ENABLE_RICH_TEXT_EDITING, enableVoiceMessages = enableVoiceMessages, appName = buildMeta.applicationName, - callState = callState, + roomCallState = roomCallState, pinnedMessagesBannerState = pinnedMessagesBannerState, eventSink = { handleEvents(it) } ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt index 2dc43030a4..a643784f1d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt @@ -18,6 +18,7 @@ import io.element.android.features.messages.impl.timeline.components.reactionsum import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState +import io.element.android.features.roomcall.api.RoomCallState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage @@ -46,14 +47,8 @@ data class MessagesState( val showReinvitePrompt: Boolean, val enableTextFormatting: Boolean, val enableVoiceMessages: Boolean, - val callState: RoomCallState, + val roomCallState: RoomCallState, val appName: String, val pinnedMessagesBannerState: PinnedMessagesBannerState, val eventSink: (MessagesEvents) -> Unit ) - -enum class RoomCallState { - ENABLED, - ONGOING, - DISABLED -} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index 5d7ac33e5e..e8dc5329f4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -33,6 +33,9 @@ import io.element.android.features.messages.impl.timeline.protection.aTimelinePr import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessageComposerState import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessagePreviewState +import io.element.android.features.roomcall.api.RoomCallState +import io.element.android.features.roomcall.api.aStandByCallState +import io.element.android.features.roomcall.api.anOngoingCallState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize @@ -70,7 +73,7 @@ open class MessagesStateProvider : PreviewParameterProvider { ), ), aMessagesState( - callState = RoomCallState.ONGOING, + roomCallState = anOngoingCallState(), ), aMessagesState( enableVoiceMessages = true, @@ -80,7 +83,7 @@ open class MessagesStateProvider : PreviewParameterProvider { ), ), aMessagesState( - callState = RoomCallState.DISABLED, + roomCallState = aStandByCallState(canStartCall = false), ), aMessagesState( pinnedMessagesBannerState = aLoadedPinnedMessagesBannerState( @@ -115,7 +118,7 @@ fun aMessagesState( hasNetworkConnection: Boolean = true, showReinvitePrompt: Boolean = false, enableVoiceMessages: Boolean = true, - callState: RoomCallState = RoomCallState.ENABLED, + roomCallState: RoomCallState = aStandByCallState(), pinnedMessagesBannerState: PinnedMessagesBannerState = aLoadedPinnedMessagesBannerState(), eventSink: (MessagesEvents) -> Unit = {}, ) = MessagesState( @@ -139,7 +142,7 @@ fun aMessagesState( showReinvitePrompt = showReinvitePrompt, enableTextFormatting = true, enableVoiceMessages = enableVoiceMessages, - callState = callState, + roomCallState = roomCallState, appName = "Element", pinnedMessagesBannerState = pinnedMessagesBannerState, eventSink = eventSink, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index bd8ae98b2f..e5d73fbed6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -52,7 +52,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme -import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.messages.impl.actionlist.ActionListEvents import io.element.android.features.messages.impl.actionlist.ActionListView import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction @@ -69,7 +68,7 @@ import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBan import io.element.android.features.messages.impl.timeline.FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.TimelineView -import io.element.android.features.messages.impl.timeline.components.JoinCallMenuItem +import io.element.android.features.messages.impl.timeline.components.CallMenuItem import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionBottomSheet import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvents import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryEvents @@ -81,6 +80,7 @@ import io.element.android.features.messages.impl.voicemessages.composer.VoiceMes import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessagePermissionRationaleDialog import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageSendingFailedDialog import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView +import io.element.android.features.roomcall.api.RoomCallState import io.element.android.libraries.androidutils.ui.hideKeyboard import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule import io.element.android.libraries.designsystem.components.ProgressDialog @@ -93,8 +93,6 @@ import io.element.android.libraries.designsystem.components.dialogs.Confirmation import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.BottomSheetDragHandle -import io.element.android.libraries.designsystem.theme.components.Icon -import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar @@ -190,7 +188,7 @@ fun MessagesView( roomName = state.roomName.dataOrNull(), roomAvatar = state.roomAvatar.dataOrNull(), heroes = state.heroes, - callState = state.callState, + roomCallState = state.roomCallState, onBackClick = { // Since the textfield is now based on an Android view, this is no longer done automatically. // We need to hide the keyboard when navigating out of this screen. @@ -479,7 +477,7 @@ private fun MessagesViewTopBar( roomName: String?, roomAvatar: AvatarData?, heroes: ImmutableList, - callState: RoomCallState, + roomCallState: RoomCallState, onRoomDetailsClick: () -> Unit, onJoinCallClick: () -> Unit, onBackClick: () -> Unit, @@ -509,9 +507,8 @@ private fun MessagesViewTopBar( }, actions = { CallMenuItem( - isCallOngoing = callState == RoomCallState.ONGOING, - onClick = onJoinCallClick, - enabled = callState != RoomCallState.DISABLED + roomCallState = roomCallState, + onJoinCallClick = onJoinCallClick, ) Spacer(Modifier.width(8.dp)) }, @@ -519,24 +516,6 @@ private fun MessagesViewTopBar( ) } -@Composable -private fun CallMenuItem( - isCallOngoing: Boolean, - enabled: Boolean = true, - onClick: () -> Unit, -) { - if (isCallOngoing) { - JoinCallMenuItem(onJoinCallClick = onClick) - } else { - IconButton(onClick = onClick, enabled = enabled) { - Icon( - imageVector = CompoundIcons.VideoCallSolid(), - contentDescription = stringResource(CommonStrings.a11y_start_call), - ) - } - } -} - @Composable private fun RoomAvatarAndNameRow( roomName: String, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt index 4ccef0f23d..4673ae57b2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt @@ -32,6 +32,7 @@ import io.element.android.features.messages.impl.timeline.factories.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState import io.element.android.features.messages.impl.typing.TypingNotificationState +import io.element.android.features.roomcall.api.aStandByCallState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher @@ -89,7 +90,8 @@ class PinnedMessagesListPresenter @AssistedInject constructor( // We don't need to compute those values userHasPermissionToSendMessage = false, userHasPermissionToSendReaction = false, - isCallOngoing = false, + // We do not care about the call state here. + roomCallState = aStandByCallState(), // don't compute this value or the pin icon will be shown pinnedEventIds = emptyList(), typingNotificationState = TypingNotificationState( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index b40e24b88a..78ee91ed0a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -32,8 +32,8 @@ import io.element.android.features.messages.impl.typing.TypingNotificationState import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager import io.element.android.features.poll.api.actions.EndPollAction import io.element.android.features.poll.api.actions.SendPollResponseAction +import io.element.android.features.roomcall.api.RoomCallState import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UniqueId @@ -73,6 +73,7 @@ class TimelinePresenter @AssistedInject constructor( private val timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer(), private val resolveVerifiedUserSendFailurePresenter: Presenter, private val typingNotificationPresenter: Presenter, + private val roomCallStatePresenter: Presenter, ) : Presenter { @AssistedFactory interface Factory { @@ -229,14 +230,15 @@ class TimelinePresenter @AssistedInject constructor( } val typingNotificationState = typingNotificationPresenter.present() - val timelineRoomInfo by remember(typingNotificationState) { + val roomCallState = roomCallStatePresenter.present() + val timelineRoomInfo by remember(typingNotificationState, roomCallState) { derivedStateOf { TimelineRoomInfo( name = room.displayName, isDm = room.isDm, userHasPermissionToSendMessage = userHasPermissionToSendMessage, userHasPermissionToSendReaction = userHasPermissionToSendReaction, - isCallOngoing = roomInfo?.hasRoomCall.orFalse(), + roomCallState = roomCallState, pinnedEventIds = roomInfo?.pinnedEventIds.orEmpty(), typingNotificationState = typingNotificationState, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt index bfb357b579..aad5b7e354 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt @@ -12,6 +12,7 @@ import io.element.android.features.messages.impl.crypto.sendfailure.resolve.Reso import io.element.android.features.messages.impl.timeline.model.NewEventState import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.typing.TypingNotificationState +import io.element.android.features.roomcall.api.RoomCallState import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UniqueId import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield @@ -73,7 +74,7 @@ data class TimelineRoomInfo( val name: String?, val userHasPermissionToSendMessage: Boolean, val userHasPermissionToSendReaction: Boolean, - val isCallOngoing: Boolean, + val roomCallState: RoomCallState, val pinnedEventIds: List, val typingNotificationState: TypingNotificationState, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt index 6c790d7b1d..4d7677560e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt @@ -23,6 +23,7 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI import io.element.android.features.messages.impl.timeline.model.virtual.aTimelineItemDaySeparatorModel import io.element.android.features.messages.impl.typing.TypingNotificationState import io.element.android.features.messages.impl.typing.aTypingNotificationState +import io.element.android.features.roomcall.api.aStandByCallState import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.core.EventId @@ -249,7 +250,7 @@ internal fun aTimelineRoomInfo( name = name, userHasPermissionToSendMessage = userHasPermissionToSendMessage, userHasPermissionToSendReaction = true, - isCallOngoing = false, + roomCallState = aStandByCallState(), pinnedEventIds = pinnedEventIds, typingNotificationState = typingNotificationState, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/CallMenuItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/CallMenuItem.kt new file mode 100644 index 0000000000..58a3940475 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/CallMenuItem.kt @@ -0,0 +1,120 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.messages.impl.timeline.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.roomcall.api.RoomCallState +import io.element.android.features.roomcall.api.RoomCallStateProvider +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconButton +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +internal fun CallMenuItem( + roomCallState: RoomCallState, + onJoinCallClick: () -> Unit, + modifier: Modifier = Modifier, +) { + when (roomCallState) { + is RoomCallState.StandBy -> { + StandByCallMenuItem( + roomCallState = roomCallState, + onJoinCallClick = onJoinCallClick, + modifier = modifier, + ) + } + is RoomCallState.OnGoing -> { + OnGoingCallMenuItem( + roomCallState = roomCallState, + onJoinCallClick = onJoinCallClick, + modifier = modifier, + ) + } + } +} + +@Composable +private fun StandByCallMenuItem( + roomCallState: RoomCallState.StandBy, + onJoinCallClick: () -> Unit, + modifier: Modifier = Modifier, +) { + IconButton( + modifier = modifier, + onClick = onJoinCallClick, + enabled = roomCallState.canStartCall, + ) { + Icon( + imageVector = CompoundIcons.VideoCallSolid(), + contentDescription = stringResource(CommonStrings.a11y_start_call), + ) + } +} + +@Composable +private fun OnGoingCallMenuItem( + roomCallState: RoomCallState.OnGoing, + onJoinCallClick: () -> Unit, + modifier: Modifier = Modifier, +) { + if (!roomCallState.isUserInTheCall) { + Button( + onClick = onJoinCallClick, + colors = ButtonDefaults.buttonColors( + contentColor = ElementTheme.colors.bgCanvasDefault, + containerColor = ElementTheme.colors.iconAccentTertiary + ), + contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp), + modifier = modifier.heightIn(min = 36.dp), + enabled = roomCallState.canJoinCall, + ) { + Icon( + modifier = Modifier.size(20.dp), + imageVector = CompoundIcons.VideoCallSolid(), + contentDescription = null + ) + Spacer(Modifier.width(8.dp)) + Text( + text = stringResource(CommonStrings.action_join), + style = ElementTheme.typography.fontBodyMdMedium + ) + Spacer(Modifier.width(8.dp)) + } + } else { + // Else user is already in the call, hide the button. + Box(modifier) + } +} + +@PreviewsDayNight +@Composable +internal fun CallMenuItemPreview( + @PreviewParameter(RoomCallStateProvider::class) roomCallState: RoomCallState +) = ElementPreview { + CallMenuItem( + roomCallState = roomCallState, + onJoinCallClick = {} + ) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/JoinCallMenuItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/JoinCallMenuItem.kt deleted file mode 100644 index 80611ccd7d..0000000000 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/JoinCallMenuItem.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -package io.element.android.features.messages.impl.timeline.components - -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import io.element.android.compound.theme.ElementTheme -import io.element.android.compound.tokens.generated.CompoundIcons -import io.element.android.libraries.designsystem.theme.components.Icon -import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.ui.strings.CommonStrings - -@Composable -internal fun JoinCallMenuItem( - onJoinCallClick: () -> Unit, -) { - Button( - onClick = onJoinCallClick, - colors = ButtonDefaults.buttonColors( - contentColor = ElementTheme.colors.bgCanvasDefault, - containerColor = ElementTheme.colors.iconAccentTertiary - ), - contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp), - modifier = Modifier.heightIn(min = 36.dp), - ) { - Icon( - modifier = Modifier.size(20.dp), - imageVector = CompoundIcons.VideoCallSolid(), - contentDescription = null - ) - Spacer(Modifier.width(8.dp)) - Text( - text = stringResource(CommonStrings.action_join), - style = ElementTheme.typography.fontBodyMdMedium - ) - Spacer(Modifier.width(8.dp)) - } -} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemCallNotifyView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemCallNotifyView.kt index d64430e087..ca499336f9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemCallNotifyView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemCallNotifyView.kt @@ -31,6 +31,8 @@ import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent +import io.element.android.features.roomcall.api.RoomCallState +import io.element.android.features.roomcall.api.RoomCallStateProvider import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -41,7 +43,7 @@ import io.element.android.libraries.ui.strings.CommonStrings @Composable internal fun TimelineItemCallNotifyView( event: TimelineItem.Event, - isCallOngoing: Boolean, + roomCallState: RoomCallState, onLongClick: (TimelineItem.Event) -> Unit, onJoinCallClick: () -> Unit, modifier: Modifier = Modifier @@ -82,8 +84,11 @@ internal fun TimelineItemCallNotifyView( ) } } - if (isCallOngoing) { - JoinCallMenuItem(onJoinCallClick) + if (roomCallState is RoomCallState.OnGoing) { + CallMenuItem( + roomCallState = roomCallState, + onJoinCallClick = onJoinCallClick, + ) } else { Text( text = event.sentTime, @@ -101,18 +106,14 @@ internal fun TimelineItemCallNotifyView( internal fun TimelineItemCallNotifyViewPreview() { ElementPreview { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { - TimelineItemCallNotifyView( - event = aTimelineItemEvent(content = TimelineItemCallNotifyContent()), - isCallOngoing = true, - onLongClick = {}, - onJoinCallClick = {}, - ) - TimelineItemCallNotifyView( - event = aTimelineItemEvent(content = TimelineItemCallNotifyContent()), - isCallOngoing = false, - onLongClick = {}, - onJoinCallClick = {}, - ) + RoomCallStateProvider().values.forEach { roomCallState -> + TimelineItemCallNotifyView( + event = aTimelineItemEvent(content = TimelineItemCallNotifyContent()), + roomCallState = roomCallState, + onLongClick = {}, + onJoinCallClick = {}, + ) + } } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt index c7c1cb5350..13c247645b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt @@ -105,7 +105,7 @@ internal fun TimelineItemRow( TimelineItemCallNotifyView( modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp), event = timelineItem, - isCallOngoing = timelineRoomInfo.isCallOngoing, + roomCallState = timelineRoomInfo.roomCallState, onLongClick = onLongClick, onJoinCallClick = onJoinCallClick, ) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index 9c6797cfc9..8d143ff179 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -37,6 +37,7 @@ import io.element.android.features.messages.test.timeline.FakeHtmlConverterProvi import io.element.android.features.networkmonitor.test.FakeNetworkMonitor import io.element.android.features.poll.api.actions.EndPollAction import io.element.android.features.poll.test.actions.FakeEndPollAction +import io.element.android.features.roomcall.api.aStandByCallState import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter @@ -139,27 +140,6 @@ class MessagesPresenterTest { } } - @Test - fun `present - call is disabled if user cannot join it even if there is an ongoing call`() = runTest { - val room = FakeMatrixRoom( - canUserJoinCallResult = { Result.success(false) }, - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - typingNoticeResult = { Result.success(Unit) }, - canUserPinUnpinResult = { Result.success(true) }, - ).apply { - givenRoomInfo(aRoomInfo(hasRoomCall = true)) - } - val presenter = createMessagesPresenter(matrixRoom = room) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = consumeItemsUntilTimeout().last() - assertThat(initialState.callState).isEqualTo(RoomCallState.DISABLED) - } - } - @Test fun `present - handle toggling a reaction`() = runTest { val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) @@ -1030,6 +1010,7 @@ class MessagesPresenterTest { readReceiptBottomSheetPresenter = { aReadReceiptBottomSheetState() }, identityChangeStatePresenter = { anIdentityChangeState() }, pinnedMessagesBannerPresenter = { aLoadedPinnedMessagesBannerState() }, + roomCallStatePresenter = { aStandByCallState() }, networkMonitor = FakeNetworkMonitor(), snackbarDispatcher = SnackbarDispatcher(), navigator = navigator, diff --git a/features/roomcall/api/build.gradle.kts b/features/roomcall/api/build.gradle.kts new file mode 100644 index 0000000000..12a2117b16 --- /dev/null +++ b/features/roomcall/api/build.gradle.kts @@ -0,0 +1,20 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.features.roomcall.api" +} + +dependencies { + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) + implementation(libs.androidx.compose.ui.tooling.preview) +} diff --git a/features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallState.kt b/features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallState.kt new file mode 100644 index 0000000000..77c58fee2c --- /dev/null +++ b/features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallState.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.roomcall.api + +import androidx.compose.runtime.Immutable +import io.element.android.features.roomcall.api.RoomCallState.OnGoing +import io.element.android.features.roomcall.api.RoomCallState.StandBy + +@Immutable +sealed interface RoomCallState { + data class StandBy( + val canStartCall: Boolean, + ) : RoomCallState + + data class OnGoing( + val canJoinCall: Boolean, + val isUserInTheCall: Boolean, + ) : RoomCallState +} + +fun RoomCallState.hasPermissionToJoin() = when (this) { + is StandBy -> canStartCall + is OnGoing -> canJoinCall +} diff --git a/features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallStateProvider.kt b/features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallStateProvider.kt new file mode 100644 index 0000000000..dce722c2c4 --- /dev/null +++ b/features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallStateProvider.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.roomcall.api + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +open class RoomCallStateProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + aStandByCallState(), + aStandByCallState(canStartCall = false), + anOngoingCallState(), + anOngoingCallState(canJoinCall = false), + anOngoingCallState(canJoinCall = true, isUserInTheCall = true), + ) +} + +fun anOngoingCallState( + canJoinCall: Boolean = true, + isUserInTheCall: Boolean = false, +) = RoomCallState.OnGoing( + canJoinCall = canJoinCall, + isUserInTheCall = isUserInTheCall, +) + +fun aStandByCallState( + canStartCall: Boolean = true, +) = RoomCallState.StandBy( + canStartCall = canStartCall, +) diff --git a/features/roomcall/impl/build.gradle.kts b/features/roomcall/impl/build.gradle.kts new file mode 100644 index 0000000000..e8f65b160b --- /dev/null +++ b/features/roomcall/impl/build.gradle.kts @@ -0,0 +1,36 @@ +import extension.setupAnvil + +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +plugins { + id("io.element.android-compose-library") +} + +android { + namespace = "io.element.android.features.roomcall.impl" +} + +setupAnvil() + +dependencies { + api(projects.features.roomcall.api) + implementation(libs.kotlinx.collections.immutable) + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrixui) + + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) + testImplementation(projects.libraries.matrix.test) + testImplementation(projects.tests.testutils) + testImplementation(libs.androidx.compose.ui.test.junit) + testReleaseImplementation(libs.androidx.compose.ui.test.manifest) +} diff --git a/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt b/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt new file mode 100644 index 0000000000..175592af08 --- /dev/null +++ b/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.roomcall.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import io.element.android.features.roomcall.api.RoomCallState +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.ui.room.canCall +import javax.inject.Inject + +class RoomCallStatePresenter @Inject constructor( + private val room: MatrixRoom, +) : Presenter { + @Composable + override fun present(): RoomCallState { + val roomInfo by room.roomInfoFlow.collectAsState(null) + val syncUpdateFlow = room.syncUpdateFlow.collectAsState() + val canJoinCall by room.canCall(updateKey = syncUpdateFlow.value) + val isUserInTheCall by remember { + derivedStateOf { + room.sessionId in roomInfo?.activeRoomCallParticipants.orEmpty() + } + } + val callState = when { + roomInfo?.hasRoomCall == true -> RoomCallState.OnGoing( + canJoinCall = canJoinCall, + isUserInTheCall = isUserInTheCall, + ) + else -> RoomCallState.StandBy(canStartCall = canJoinCall) + } + return callState + } +} diff --git a/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/di/RoomCallModule.kt b/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/di/RoomCallModule.kt new file mode 100644 index 0000000000..34c8d2448f --- /dev/null +++ b/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/di/RoomCallModule.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.roomcall.impl.di + +import com.squareup.anvil.annotations.ContributesTo +import dagger.Binds +import dagger.Module +import io.element.android.features.roomcall.api.RoomCallState +import io.element.android.features.roomcall.impl.RoomCallStatePresenter +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.di.RoomScope + +@ContributesTo(RoomScope::class) +@Module +interface RoomCallModule { + @Binds + fun bindRoomCallStatePresenter(presenter: RoomCallStatePresenter): Presenter +} diff --git a/features/roomcall/impl/src/test/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenterTest.kt b/features/roomcall/impl/src/test/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenterTest.kt new file mode 100644 index 0000000000..ae6b062738 --- /dev/null +++ b/features/roomcall/impl/src/test/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenterTest.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.roomcall.impl + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.roomcall.api.RoomCallState +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.aRoomInfo +import io.element.android.tests.testutils.test +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class RoomCallStatePresenterTest { + @Test + fun `present - call is disabled if user cannot join it even if there is an ongoing call`() = runTest { + val room = FakeMatrixRoom( + canUserJoinCallResult = { Result.success(false) }, + canUserSendMessageResult = { _, _ -> Result.success(true) }, + canRedactOwnResult = { Result.success(true) }, + canRedactOtherResult = { Result.success(true) }, + typingNoticeResult = { Result.success(Unit) }, + canUserPinUnpinResult = { Result.success(true) }, + ).apply { + givenRoomInfo(aRoomInfo(hasRoomCall = true)) + } + val presenter = createRoomCallStatePresenter(matrixRoom = room) + presenter.test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo(RoomCallState.OnGoing(canJoinCall = false)) + } + } + + private fun createRoomCallStatePresenter( + matrixRoom: MatrixRoom + ): RoomCallStatePresenter { + return RoomCallStatePresenter( + room = matrixRoom, + ) + } +} diff --git a/features/roomdetails/impl/build.gradle.kts b/features/roomdetails/impl/build.gradle.kts index 42f27963f3..231161e583 100644 --- a/features/roomdetails/impl/build.gradle.kts +++ b/features/roomdetails/impl/build.gradle.kts @@ -49,6 +49,7 @@ dependencies { implementation(projects.services.analytics.compose) implementation(projects.features.poll.api) implementation(projects.features.messages.api) + implementation(projects.features.roomcall.api) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt index eccc8dd9d2..586790b618 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt @@ -21,6 +21,7 @@ import im.vector.app.features.analytics.plan.Interaction import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.messages.api.pinned.IsPinnedMessagesFeatureEnabled +import io.element.android.features.roomcall.api.RoomCallState import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.bool.orFalse @@ -37,7 +38,6 @@ import io.element.android.libraries.matrix.api.room.isDm import io.element.android.libraries.matrix.api.room.powerlevels.canInvite import io.element.android.libraries.matrix.api.room.powerlevels.canSendState import io.element.android.libraries.matrix.api.room.roomNotificationSettings -import io.element.android.libraries.matrix.ui.room.canCall import io.element.android.libraries.matrix.ui.room.getCurrentRoomMember import io.element.android.libraries.matrix.ui.room.getDirectRoomMember import io.element.android.libraries.matrix.ui.room.isOwnUserAdmin @@ -57,6 +57,7 @@ class RoomDetailsPresenter @Inject constructor( private val notificationSettingsService: NotificationSettingsService, private val roomMembersDetailsPresenterFactory: RoomMemberDetailsPresenter.Factory, private val leaveRoomPresenter: Presenter, + private val roomCallStatePresenter: Presenter, private val dispatchers: CoroutineDispatchers, private val analyticsService: AnalyticsService, private val isPinnedMessagesFeatureEnabled: IsPinnedMessagesFeatureEnabled, @@ -87,18 +88,16 @@ class RoomDetailsPresenter @Inject constructor( } } - val syncUpdateTimestamp by room.syncUpdateFlow.collectAsState() - val membersState by room.membersStateFlow.collectAsState() val canInvite by getCanInvite(membersState) val canEditName by getCanSendState(membersState, StateEventType.ROOM_NAME) val canEditAvatar by getCanSendState(membersState, StateEventType.ROOM_AVATAR) val canEditTopic by getCanSendState(membersState, StateEventType.ROOM_TOPIC) - val canJoinCall by room.canCall(updateKey = syncUpdateTimestamp) val dmMember by room.getDirectRoomMember(membersState) val currentMember by room.getCurrentRoomMember(membersState) val roomMemberDetailsPresenter = roomMemberDetailsPresenter(dmMember) val roomType by getRoomType(dmMember, currentMember) + val roomCallState = roomCallStatePresenter.present() val topicState = remember(canEditTopic, roomTopic, roomType) { val topic = roomTopic @@ -143,7 +142,7 @@ class RoomDetailsPresenter @Inject constructor( canInvite = canInvite, canEdit = (canEditAvatar || canEditName || canEditTopic) && roomType == RoomDetailsType.Room, canShowNotificationSettings = canShowNotificationSettings.value, - canCall = canJoinCall, + roomCallState = roomCallState, roomType = roomType, roomMemberDetailsState = roomMemberDetailsState, leaveRoomState = leaveRoomState, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt index 2208748563..d43b0a813a 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt @@ -9,6 +9,7 @@ package io.element.android.features.roomdetails.impl import androidx.compose.runtime.Immutable import io.element.android.features.leaveroom.api.LeaveRoomState +import io.element.android.features.roomcall.api.RoomCallState import io.element.android.features.userprofile.api.UserProfileState import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomId @@ -31,7 +32,7 @@ data class RoomDetailsState( val canEdit: Boolean, val canInvite: Boolean, val canShowNotificationSettings: Boolean, - val canCall: Boolean, + val roomCallState: RoomCallState, val leaveRoomState: LeaveRoomState, val roomNotificationSettings: RoomNotificationSettings?, val isFavorite: Boolean, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt index 7e71d2b39f..49b9f73cb5 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt @@ -10,6 +10,8 @@ package io.element.android.features.roomdetails.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.leaveroom.api.aLeaveRoomState +import io.element.android.features.roomcall.api.RoomCallState +import io.element.android.features.roomcall.api.aStandByCallState import io.element.android.features.roomdetails.impl.members.aRoomMember import io.element.android.features.userprofile.api.UserProfileState import io.element.android.features.userprofile.shared.aUserProfileState @@ -42,7 +44,7 @@ open class RoomDetailsStateProvider : PreviewParameterProvider // Also test the roomNotificationSettings ALL_MESSAGES in the same screenshot. Icon 'Mute' should be displayed roomNotificationSettings = aRoomNotificationSettings(mode = RoomNotificationMode.ALL_MESSAGES, isDefault = true) ), - aRoomDetailsState(canCall = false, canInvite = false), + aRoomDetailsState(roomCallState = aStandByCallState(false), canInvite = false), aRoomDetailsState(isPublic = false), aRoomDetailsState(heroes = aMatrixUserList()), aRoomDetailsState(pinnedMessagesCount = 3), @@ -89,7 +91,7 @@ fun aRoomDetailsState( canInvite: Boolean = false, canEdit: Boolean = false, canShowNotificationSettings: Boolean = true, - canCall: Boolean = true, + roomCallState: RoomCallState = aStandByCallState(), roomType: RoomDetailsType = RoomDetailsType.Room, roomMemberDetailsState: UserProfileState? = null, leaveRoomState: LeaveRoomState = aLeaveRoomState(), @@ -112,7 +114,7 @@ fun aRoomDetailsState( canInvite = canInvite, canEdit = canEdit, canShowNotificationSettings = canShowNotificationSettings, - canCall = canCall, + roomCallState = roomCallState, roomType = roomType, roomMemberDetailsState = roomMemberDetailsState, leaveRoomState = leaveRoomState, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt index 163a2c5fd5..7e89c3ef07 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt @@ -42,6 +42,7 @@ import im.vector.app.features.analytics.plan.Interaction import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.leaveroom.api.LeaveRoomView +import io.element.android.features.roomcall.api.hasPermissionToJoin import io.element.android.features.userprofile.shared.blockuser.BlockUserDialogs import io.element.android.features.userprofile.shared.blockuser.BlockUserSection import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage @@ -299,7 +300,8 @@ private fun MainActionsSection( ) } } - if (state.canCall) { + if (state.roomCallState.hasPermissionToJoin()) { + // TODO Improve the view depending on all the cases here? MainActionButton( title = stringResource(CommonStrings.action_call), imageVector = CompoundIcons.VideoCall(), diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenterTest.kt index ea83f89181..b41090b661 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenterTest.kt @@ -17,6 +17,7 @@ import im.vector.app.features.analytics.plan.Interaction import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.leaveroom.api.aLeaveRoomState +import io.element.android.features.roomcall.api.aStandByCallState import io.element.android.features.roomdetails.aMatrixRoom import io.element.android.features.roomdetails.impl.members.aRoomMember import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter @@ -98,6 +99,7 @@ class RoomDetailsPresenterTest { notificationSettingsService = matrixClient.notificationSettingsService(), roomMembersDetailsPresenterFactory = roomMemberDetailsPresenterFactory, leaveRoomPresenter = { leaveRoomState }, + roomCallStatePresenter = { aStandByCallState() }, dispatchers = dispatchers, isPinnedMessagesFeatureEnabled = { isPinnedMessagesFeatureEnabled }, analyticsService = analyticsService, From 1c78f96148b2e5d737979db00275e027e8e5411a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 6 Nov 2024 09:43:38 +0100 Subject: [PATCH 2/6] Ensure the user can join the call even if they has joined a call in another session. --- .../android/features/call/api/CurrentCall.kt | 25 ++++++++++++++++ .../features/call/api/CurrentCallObserver.kt | 19 ++++++++++++ .../call/impl/utils/ActiveCallManager.kt | 25 ++++++++++++++++ .../impl/utils/DefaultCurrentCallObserver.kt | 30 +++++++++++++++++++ .../utils/DefaultActiveCallManagerTest.kt | 2 ++ .../impl/timeline/components/CallMenuItem.kt | 2 +- .../features/roomcall/api/RoomCallState.kt | 1 + .../roomcall/api/RoomCallStateProvider.kt | 2 ++ features/roomcall/impl/build.gradle.kts | 1 + .../roomcall/impl/RoomCallStatePresenter.kt | 10 +++++++ 10 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 features/call/api/src/main/kotlin/io/element/android/features/call/api/CurrentCall.kt create mode 100644 features/call/api/src/main/kotlin/io/element/android/features/call/api/CurrentCallObserver.kt create mode 100644 features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCurrentCallObserver.kt diff --git a/features/call/api/src/main/kotlin/io/element/android/features/call/api/CurrentCall.kt b/features/call/api/src/main/kotlin/io/element/android/features/call/api/CurrentCall.kt new file mode 100644 index 0000000000..387d4a98ad --- /dev/null +++ b/features/call/api/src/main/kotlin/io/element/android/features/call/api/CurrentCall.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.call.api + +import io.element.android.libraries.matrix.api.core.RoomId + +/** + * Value for the local current call. + */ +sealed interface CurrentCall { + data object None : CurrentCall + + data class RoomCall( + val roomId: RoomId, + ) : CurrentCall + + data class ExternalUrl( + val url: String, + ) : CurrentCall +} diff --git a/features/call/api/src/main/kotlin/io/element/android/features/call/api/CurrentCallObserver.kt b/features/call/api/src/main/kotlin/io/element/android/features/call/api/CurrentCallObserver.kt new file mode 100644 index 0000000000..c0f8eb35a8 --- /dev/null +++ b/features/call/api/src/main/kotlin/io/element/android/features/call/api/CurrentCallObserver.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.call.api + +import kotlinx.coroutines.flow.StateFlow + +interface CurrentCallObserver { + /** + * The current call state flow, which will be updated when the active call changes. + * This value reflect the local state of the call. It is not updated if the user answers + * a call from another session. + */ + val currentCall: StateFlow +} diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt index 45f1f2be63..ccdbc1b91a 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt @@ -12,6 +12,7 @@ import androidx.core.app.NotificationManagerCompat import com.squareup.anvil.annotations.ContributesBinding import io.element.android.appconfig.ElementCallConfig import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.CurrentCall import io.element.android.features.call.impl.notifications.CallNotificationData import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator import io.element.android.libraries.di.AppScope @@ -82,6 +83,7 @@ class DefaultActiveCallManager @Inject constructor( private val ringingCallNotificationCreator: RingingCallNotificationCreator, private val notificationManagerCompat: NotificationManagerCompat, private val matrixClientProvider: MatrixClientProvider, + private val defaultCurrentCallObserver: DefaultCurrentCallObserver, ) : ActiveCallManager { private var timedOutCallJob: Job? = null @@ -89,6 +91,7 @@ class DefaultActiveCallManager @Inject constructor( init { observeRingingCall() + observeCurrentCall() } override fun registerIncomingCall(notificationData: CallNotificationData) { @@ -209,6 +212,28 @@ class DefaultActiveCallManager @Inject constructor( } .launchIn(coroutineScope) } + + private fun observeCurrentCall() { + activeCall + .onEach { value -> + if (value == null) { + defaultCurrentCallObserver.onCallEnded() + } else { + when (value.callState) { + is CallState.Ringing -> { + // Nothing to do + } + is CallState.InCall -> { + when (val callType = value.callType) { + is CallType.ExternalUrl -> defaultCurrentCallObserver.onCallStarted(CurrentCall.ExternalUrl(callType.url)) + is CallType.RoomCall -> defaultCurrentCallObserver.onCallStarted(CurrentCall.RoomCall(callType.roomId)) + } + } + } + } + } + .launchIn(coroutineScope) + } } /** diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCurrentCallObserver.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCurrentCallObserver.kt new file mode 100644 index 0000000000..4810d31ee6 --- /dev/null +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCurrentCallObserver.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.call.impl.utils + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.call.api.CurrentCall +import io.element.android.features.call.api.CurrentCallObserver +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.SingleIn +import kotlinx.coroutines.flow.MutableStateFlow +import javax.inject.Inject + +@SingleIn(AppScope::class) +@ContributesBinding(AppScope::class) +class DefaultCurrentCallObserver @Inject constructor() : CurrentCallObserver { + override val currentCall = MutableStateFlow(CurrentCall.None) + + fun onCallStarted(call: CurrentCall) { + currentCall.value = call + } + + fun onCallEnded() { + currentCall.value = CurrentCall.None + } +} diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt index 45568f2d39..93144ccd50 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt @@ -15,6 +15,7 @@ import io.element.android.features.call.impl.notifications.RingingCallNotificati import io.element.android.features.call.impl.utils.ActiveCall import io.element.android.features.call.impl.utils.CallState import io.element.android.features.call.impl.utils.DefaultActiveCallManager +import io.element.android.features.call.impl.utils.DefaultCurrentCallObserver import io.element.android.features.call.test.aCallNotificationData import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId @@ -299,5 +300,6 @@ class DefaultActiveCallManagerTest { ), notificationManagerCompat = notificationManagerCompat, matrixClientProvider = matrixClientProvider, + defaultCurrentCallObserver = DefaultCurrentCallObserver(), ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/CallMenuItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/CallMenuItem.kt index 58a3940475..2284ffa50c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/CallMenuItem.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/CallMenuItem.kt @@ -79,7 +79,7 @@ private fun OnGoingCallMenuItem( onJoinCallClick: () -> Unit, modifier: Modifier = Modifier, ) { - if (!roomCallState.isUserInTheCall) { + if (!roomCallState.isUserLocallyInTheCall) { Button( onClick = onJoinCallClick, colors = ButtonDefaults.buttonColors( diff --git a/features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallState.kt b/features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallState.kt index 77c58fee2c..e47a623914 100644 --- a/features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallState.kt +++ b/features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallState.kt @@ -20,6 +20,7 @@ sealed interface RoomCallState { data class OnGoing( val canJoinCall: Boolean, val isUserInTheCall: Boolean, + val isUserLocallyInTheCall: Boolean, ) : RoomCallState } diff --git a/features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallStateProvider.kt b/features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallStateProvider.kt index dce722c2c4..6351c25479 100644 --- a/features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallStateProvider.kt +++ b/features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallStateProvider.kt @@ -22,9 +22,11 @@ open class RoomCallStateProvider : PreviewParameterProvider { fun anOngoingCallState( canJoinCall: Boolean = true, isUserInTheCall: Boolean = false, + isUserLocallyInTheCall: Boolean = isUserInTheCall, ) = RoomCallState.OnGoing( canJoinCall = canJoinCall, isUserInTheCall = isUserInTheCall, + isUserLocallyInTheCall = isUserLocallyInTheCall, ) fun aStandByCallState( diff --git a/features/roomcall/impl/build.gradle.kts b/features/roomcall/impl/build.gradle.kts index e8f65b160b..adb5b07bf2 100644 --- a/features/roomcall/impl/build.gradle.kts +++ b/features/roomcall/impl/build.gradle.kts @@ -20,6 +20,7 @@ setupAnvil() dependencies { api(projects.features.roomcall.api) implementation(libs.kotlinx.collections.immutable) + implementation(projects.features.call.api) implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixui) diff --git a/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt b/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt index 175592af08..b6eb10b342 100644 --- a/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt +++ b/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt @@ -12,6 +12,8 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import io.element.android.features.call.api.CurrentCall +import io.element.android.features.call.api.CurrentCallObserver import io.element.android.features.roomcall.api.RoomCallState import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.room.MatrixRoom @@ -20,6 +22,7 @@ import javax.inject.Inject class RoomCallStatePresenter @Inject constructor( private val room: MatrixRoom, + private val currentCallObserver: CurrentCallObserver, ) : Presenter { @Composable override fun present(): RoomCallState { @@ -31,10 +34,17 @@ class RoomCallStatePresenter @Inject constructor( room.sessionId in roomInfo?.activeRoomCallParticipants.orEmpty() } } + val currentCall by currentCallObserver.currentCall.collectAsState() + val isUserLocallyInTheCall by remember { + derivedStateOf { + (currentCall as? CurrentCall.RoomCall)?.roomId == room.roomId + } + } val callState = when { roomInfo?.hasRoomCall == true -> RoomCallState.OnGoing( canJoinCall = canJoinCall, isUserInTheCall = isUserInTheCall, + isUserLocallyInTheCall = isUserLocallyInTheCall, ) else -> RoomCallState.StandBy(canStartCall = canJoinCall) } From 12e7172eb687908bd38c03bbf29480963f5bd83c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 6 Nov 2024 10:11:50 +0100 Subject: [PATCH 3/6] Update tests --- .../call/test/FakeCurrentCallObserver.kt | 22 +++ .../impl/timeline/TimelinePresenterTest.kt | 2 + features/roomcall/impl/build.gradle.kts | 1 + .../impl/RoomCallStatePresenterTest.kt | 169 +++++++++++++++++- 4 files changed, 186 insertions(+), 8 deletions(-) create mode 100644 features/call/test/src/main/kotlin/io/element/android/features/call/test/FakeCurrentCallObserver.kt diff --git a/features/call/test/src/main/kotlin/io/element/android/features/call/test/FakeCurrentCallObserver.kt b/features/call/test/src/main/kotlin/io/element/android/features/call/test/FakeCurrentCallObserver.kt new file mode 100644 index 0000000000..d153047c66 --- /dev/null +++ b/features/call/test/src/main/kotlin/io/element/android/features/call/test/FakeCurrentCallObserver.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.call.test + +import io.element.android.features.call.api.CurrentCall +import io.element.android.features.call.api.CurrentCallObserver +import kotlinx.coroutines.flow.MutableStateFlow + +class FakeCurrentCallObserver( + initialValue: CurrentCall = CurrentCall.None, +) : CurrentCallObserver { + override val currentCall = MutableStateFlow(initialValue) + + fun setCurrentCall(value: CurrentCall) { + currentCall.value = value + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt index 74df8ee46c..d153dd5743 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt @@ -27,6 +27,7 @@ import io.element.android.features.poll.api.actions.EndPollAction import io.element.android.features.poll.api.actions.SendPollResponseAction import io.element.android.features.poll.test.actions.FakeEndPollAction import io.element.android.features.poll.test.actions.FakeSendPollResponseAction +import io.element.android.features.roomcall.api.aStandByCallState import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UniqueId import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState @@ -685,5 +686,6 @@ internal fun TestScope.createTimelinePresenter( timelineController = TimelineController(room), resolveVerifiedUserSendFailurePresenter = { aResolveVerifiedUserSendFailureState() }, typingNotificationPresenter = { aTypingNotificationState() }, + roomCallStatePresenter = { aStandByCallState() }, ) } diff --git a/features/roomcall/impl/build.gradle.kts b/features/roomcall/impl/build.gradle.kts index adb5b07bf2..6ac4ff934e 100644 --- a/features/roomcall/impl/build.gradle.kts +++ b/features/roomcall/impl/build.gradle.kts @@ -31,6 +31,7 @@ dependencies { testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.features.call.test) testImplementation(projects.tests.testutils) testImplementation(libs.androidx.compose.ui.test.junit) testReleaseImplementation(libs.androidx.compose.ui.test.manifest) diff --git a/features/roomcall/impl/src/test/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenterTest.kt b/features/roomcall/impl/src/test/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenterTest.kt index ae6b062738..aa4a0eb676 100644 --- a/features/roomcall/impl/src/test/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenterTest.kt +++ b/features/roomcall/impl/src/test/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenterTest.kt @@ -8,6 +8,9 @@ package io.element.android.features.roomcall.impl import com.google.common.truth.Truth.assertThat +import io.element.android.features.call.api.CurrentCall +import io.element.android.features.call.api.CurrentCallObserver +import io.element.android.features.call.test.FakeCurrentCallObserver import io.element.android.features.roomcall.api.RoomCallState import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom @@ -17,30 +20,180 @@ import kotlinx.coroutines.test.runTest import org.junit.Test class RoomCallStatePresenterTest { + @Test + fun `present - initial state`() = runTest { + val room = FakeMatrixRoom( + canUserJoinCallResult = { Result.success(false) }, + ) + val presenter = createRoomCallStatePresenter(matrixRoom = room) + presenter.test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo( + RoomCallState.StandBy( + canStartCall = false, + ) + ) + } + } + + @Test + fun `present - initial state - user can join call`() = runTest { + val room = FakeMatrixRoom( + canUserJoinCallResult = { Result.success(true) }, + ) + val presenter = createRoomCallStatePresenter(matrixRoom = room) + presenter.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState).isEqualTo( + RoomCallState.StandBy( + canStartCall = true, + ) + ) + } + } + @Test fun `present - call is disabled if user cannot join it even if there is an ongoing call`() = runTest { val room = FakeMatrixRoom( canUserJoinCallResult = { Result.success(false) }, - canUserSendMessageResult = { _, _ -> Result.success(true) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - typingNoticeResult = { Result.success(Unit) }, - canUserPinUnpinResult = { Result.success(true) }, ).apply { givenRoomInfo(aRoomInfo(hasRoomCall = true)) } val presenter = createRoomCallStatePresenter(matrixRoom = room) presenter.test { - val initialState = awaitItem() - assertThat(initialState).isEqualTo(RoomCallState.OnGoing(canJoinCall = false)) + skipItems(1) + assertThat(awaitItem()).isEqualTo( + RoomCallState.OnGoing( + canJoinCall = false, + isUserInTheCall = false, + isUserLocallyInTheCall = false, + ) + ) + } + } + + @Test + fun `present - user has joined the call on another session`() = runTest { + val room = FakeMatrixRoom( + canUserJoinCallResult = { Result.success(true) }, + ).apply { + givenRoomInfo( + aRoomInfo( + hasRoomCall = true, + activeRoomCallParticipants = listOf(sessionId), + ) + ) + } + val presenter = createRoomCallStatePresenter(matrixRoom = room) + presenter.test { + skipItems(1) + assertThat(awaitItem()).isEqualTo( + RoomCallState.OnGoing( + canJoinCall = true, + isUserInTheCall = true, + isUserLocallyInTheCall = false, + ) + ) + } + } + + @Test + fun `present - user has joined the call locally`() = runTest { + val room = FakeMatrixRoom( + canUserJoinCallResult = { Result.success(true) }, + ).apply { + givenRoomInfo( + aRoomInfo( + hasRoomCall = true, + activeRoomCallParticipants = listOf(sessionId), + ) + ) + } + val presenter = createRoomCallStatePresenter( + matrixRoom = room, + currentCallObserver = FakeCurrentCallObserver(initialValue = CurrentCall.RoomCall(room.roomId)), + ) + presenter.test { + skipItems(1) + assertThat(awaitItem()).isEqualTo( + RoomCallState.OnGoing( + canJoinCall = true, + isUserInTheCall = true, + isUserLocallyInTheCall = true, + ) + ) + } + } + + @Test + fun `present - user leaves the call`() = runTest { + val room = FakeMatrixRoom( + canUserJoinCallResult = { Result.success(true) }, + ).apply { + givenRoomInfo( + aRoomInfo( + hasRoomCall = true, + activeRoomCallParticipants = listOf(sessionId), + ) + ) + } + val currentCallObserver = FakeCurrentCallObserver(initialValue = CurrentCall.RoomCall(room.roomId)) + val presenter = createRoomCallStatePresenter( + matrixRoom = room, + currentCallObserver = currentCallObserver + ) + presenter.test { + skipItems(1) + assertThat(awaitItem()).isEqualTo( + RoomCallState.OnGoing( + canJoinCall = true, + isUserInTheCall = true, + isUserLocallyInTheCall = true, + ) + ) + currentCallObserver.setCurrentCall(CurrentCall.None) + assertThat(awaitItem()).isEqualTo( + RoomCallState.OnGoing( + canJoinCall = true, + isUserInTheCall = true, + isUserLocallyInTheCall = false, + ) + ) + room.givenRoomInfo( + aRoomInfo( + hasRoomCall = true, + activeRoomCallParticipants = emptyList(), + ) + ) + assertThat(awaitItem()).isEqualTo( + RoomCallState.OnGoing( + canJoinCall = true, + isUserInTheCall = false, + isUserLocallyInTheCall = false, + ) + ) + room.givenRoomInfo( + aRoomInfo( + hasRoomCall = false, + activeRoomCallParticipants = emptyList(), + ) + ) + assertThat(awaitItem()).isEqualTo( + RoomCallState.StandBy( + canStartCall = true, + ) + ) } } private fun createRoomCallStatePresenter( - matrixRoom: MatrixRoom + matrixRoom: MatrixRoom, + currentCallObserver: CurrentCallObserver = FakeCurrentCallObserver(), ): RoomCallStatePresenter { return RoomCallStatePresenter( room = matrixRoom, + currentCallObserver = currentCallObserver, ) } } From 4c9662887fd8db4830de21808ab03c073de46fb3 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Wed, 6 Nov 2024 09:31:49 +0000 Subject: [PATCH 4/6] Update screenshots --- ...essages.impl.timeline.components_CallMenuItem_Day_0_en.png | 3 +++ ...essages.impl.timeline.components_CallMenuItem_Day_1_en.png | 3 +++ ...essages.impl.timeline.components_CallMenuItem_Day_2_en.png | 3 +++ ...essages.impl.timeline.components_CallMenuItem_Day_3_en.png | 3 +++ ...essages.impl.timeline.components_CallMenuItem_Day_4_en.png | 3 +++ ...sages.impl.timeline.components_CallMenuItem_Night_0_en.png | 3 +++ ...sages.impl.timeline.components_CallMenuItem_Night_1_en.png | 3 +++ ...sages.impl.timeline.components_CallMenuItem_Night_2_en.png | 3 +++ ...sages.impl.timeline.components_CallMenuItem_Night_3_en.png | 3 +++ ...sages.impl.timeline.components_CallMenuItem_Night_4_en.png | 3 +++ ...imeline.components_TimelineItemCallNotifyView_Day_0_en.png | 4 ++-- ...eline.components_TimelineItemCallNotifyView_Night_0_en.png | 4 ++-- 12 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Day_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Day_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Day_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Day_4_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Night_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Night_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Night_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Night_4_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Day_0_en.png new file mode 100644 index 0000000000..4b5528411e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a85dfe911ca378a94b3a8fab85efab4b0915d330e804ded3472c6e6a101a9f8 +size 4016 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Day_1_en.png new file mode 100644 index 0000000000..09f9005925 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f135ec4c24292d06f853d0ca17b90b61a8fab144e72021862152966cd750ecf9 +size 3970 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Day_2_en.png new file mode 100644 index 0000000000..d5b37d48d6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b512a152991e335a54d75d83a9190c87e09aaff34aa05d57fea3c585767cb1c5 +size 5658 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Day_3_en.png new file mode 100644 index 0000000000..653139d523 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ca4e84ced7c4ce27b4ff5319387756a3990edd2a8861a3b998ec5ed8705ece7 +size 5381 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Day_4_en.png new file mode 100644 index 0000000000..1b6fb4bab8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:96a867cb12498cbdc97957bee07855dfaa13602baddaf933aff2b666ef4c7650 +size 3642 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Night_0_en.png new file mode 100644 index 0000000000..29d29ebd5f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:75256919486ff2345c7db724caf5ee3767bbfa1d4a757c3ade9fb83802a65923 +size 4056 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Night_1_en.png new file mode 100644 index 0000000000..4dcdf87172 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e124fbad6f6dd867353937772b0911ec2ce521344a4acb9af1d8ade941ca0d5 +size 3980 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Night_2_en.png new file mode 100644 index 0000000000..edf7311f7f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5777c1bc314b886fe1b195a06c27ed8144464c466fa63c9a7fd6f00d20f8ebf0 +size 5433 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Night_3_en.png new file mode 100644 index 0000000000..34f71876f3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63a67d21058f9a0a9077c0a8c774ab3f5aff9dcee2dbdd197b571682296ba598 +size 5314 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Night_4_en.png new file mode 100644 index 0000000000..d6fd8eeb70 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_CallMenuItem_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5bb36ccd718f3fec5b04f1bc812dc7718b5ea7fa4619c8b031466297a8d016fd +size 3659 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemCallNotifyView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemCallNotifyView_Day_0_en.png index 464ad64ddd..b5371b0448 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemCallNotifyView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemCallNotifyView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2246cd94deba78469805df1a8ba4aa36fafee4179a5d124eb555e7f9408442e -size 18644 +oid sha256:178eef1c52bac961c9f29bc8c4df63923c8441884b1c717c9e516b9621ff3230 +size 37944 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemCallNotifyView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemCallNotifyView_Night_0_en.png index 5936fd898b..b37717cfc7 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemCallNotifyView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemCallNotifyView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:029ffeafb441ebc800ba531c927cfa73259a04e06a04724310a0440238442b0e -size 18515 +oid sha256:9445d05edb98b6c3bed2a2aa3354031b7e116bf7753f9aa85b4db388751154a3 +size 38154 From 2b5acb30232d026fc71f9e7e00a3c228ce370cb3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 6 Nov 2024 17:24:26 +0100 Subject: [PATCH 5/6] Rename CurrentCallObserver to CurrentCallService --- ...rentCallObserver.kt => CurrentCallService.kt} | 2 +- .../call/impl/utils/ActiveCallManager.kt | 8 ++++---- ...lObserver.kt => DefaultCurrentCallService.kt} | 4 ++-- .../call/utils/DefaultActiveCallManagerTest.kt | 4 ++-- ...CallObserver.kt => FakeCurrentCallService.kt} | 6 +++--- .../roomcall/impl/RoomCallStatePresenter.kt | 6 +++--- .../roomcall/impl/RoomCallStatePresenterTest.kt | 16 ++++++++-------- 7 files changed, 23 insertions(+), 23 deletions(-) rename features/call/api/src/main/kotlin/io/element/android/features/call/api/{CurrentCallObserver.kt => CurrentCallService.kt} (94%) rename features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/{DefaultCurrentCallObserver.kt => DefaultCurrentCallService.kt} (84%) rename features/call/test/src/main/kotlin/io/element/android/features/call/test/{FakeCurrentCallObserver.kt => FakeCurrentCallService.kt} (80%) diff --git a/features/call/api/src/main/kotlin/io/element/android/features/call/api/CurrentCallObserver.kt b/features/call/api/src/main/kotlin/io/element/android/features/call/api/CurrentCallService.kt similarity index 94% rename from features/call/api/src/main/kotlin/io/element/android/features/call/api/CurrentCallObserver.kt rename to features/call/api/src/main/kotlin/io/element/android/features/call/api/CurrentCallService.kt index c0f8eb35a8..9cc61ab96d 100644 --- a/features/call/api/src/main/kotlin/io/element/android/features/call/api/CurrentCallObserver.kt +++ b/features/call/api/src/main/kotlin/io/element/android/features/call/api/CurrentCallService.kt @@ -9,7 +9,7 @@ package io.element.android.features.call.api import kotlinx.coroutines.flow.StateFlow -interface CurrentCallObserver { +interface CurrentCallService { /** * The current call state flow, which will be updated when the active call changes. * This value reflect the local state of the call. It is not updated if the user answers diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt index ccdbc1b91a..d774a5133c 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt @@ -83,7 +83,7 @@ class DefaultActiveCallManager @Inject constructor( private val ringingCallNotificationCreator: RingingCallNotificationCreator, private val notificationManagerCompat: NotificationManagerCompat, private val matrixClientProvider: MatrixClientProvider, - private val defaultCurrentCallObserver: DefaultCurrentCallObserver, + private val defaultCurrentCallService: DefaultCurrentCallService, ) : ActiveCallManager { private var timedOutCallJob: Job? = null @@ -217,7 +217,7 @@ class DefaultActiveCallManager @Inject constructor( activeCall .onEach { value -> if (value == null) { - defaultCurrentCallObserver.onCallEnded() + defaultCurrentCallService.onCallEnded() } else { when (value.callState) { is CallState.Ringing -> { @@ -225,8 +225,8 @@ class DefaultActiveCallManager @Inject constructor( } is CallState.InCall -> { when (val callType = value.callType) { - is CallType.ExternalUrl -> defaultCurrentCallObserver.onCallStarted(CurrentCall.ExternalUrl(callType.url)) - is CallType.RoomCall -> defaultCurrentCallObserver.onCallStarted(CurrentCall.RoomCall(callType.roomId)) + is CallType.ExternalUrl -> defaultCurrentCallService.onCallStarted(CurrentCall.ExternalUrl(callType.url)) + is CallType.RoomCall -> defaultCurrentCallService.onCallStarted(CurrentCall.RoomCall(callType.roomId)) } } } diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCurrentCallObserver.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCurrentCallService.kt similarity index 84% rename from features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCurrentCallObserver.kt rename to features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCurrentCallService.kt index 4810d31ee6..a2a358483b 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCurrentCallObserver.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCurrentCallService.kt @@ -9,7 +9,7 @@ package io.element.android.features.call.impl.utils import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.call.api.CurrentCall -import io.element.android.features.call.api.CurrentCallObserver +import io.element.android.features.call.api.CurrentCallService import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn import kotlinx.coroutines.flow.MutableStateFlow @@ -17,7 +17,7 @@ import javax.inject.Inject @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) -class DefaultCurrentCallObserver @Inject constructor() : CurrentCallObserver { +class DefaultCurrentCallService @Inject constructor() : CurrentCallService { override val currentCall = MutableStateFlow(CurrentCall.None) fun onCallStarted(call: CurrentCall) { diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt index 93144ccd50..b9f6a524ae 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt @@ -15,7 +15,7 @@ import io.element.android.features.call.impl.notifications.RingingCallNotificati import io.element.android.features.call.impl.utils.ActiveCall import io.element.android.features.call.impl.utils.CallState import io.element.android.features.call.impl.utils.DefaultActiveCallManager -import io.element.android.features.call.impl.utils.DefaultCurrentCallObserver +import io.element.android.features.call.impl.utils.DefaultCurrentCallService import io.element.android.features.call.test.aCallNotificationData import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId @@ -300,6 +300,6 @@ class DefaultActiveCallManagerTest { ), notificationManagerCompat = notificationManagerCompat, matrixClientProvider = matrixClientProvider, - defaultCurrentCallObserver = DefaultCurrentCallObserver(), + defaultCurrentCallService = DefaultCurrentCallService(), ) } diff --git a/features/call/test/src/main/kotlin/io/element/android/features/call/test/FakeCurrentCallObserver.kt b/features/call/test/src/main/kotlin/io/element/android/features/call/test/FakeCurrentCallService.kt similarity index 80% rename from features/call/test/src/main/kotlin/io/element/android/features/call/test/FakeCurrentCallObserver.kt rename to features/call/test/src/main/kotlin/io/element/android/features/call/test/FakeCurrentCallService.kt index d153047c66..ff0a9e9096 100644 --- a/features/call/test/src/main/kotlin/io/element/android/features/call/test/FakeCurrentCallObserver.kt +++ b/features/call/test/src/main/kotlin/io/element/android/features/call/test/FakeCurrentCallService.kt @@ -8,12 +8,12 @@ package io.element.android.features.call.test import io.element.android.features.call.api.CurrentCall -import io.element.android.features.call.api.CurrentCallObserver +import io.element.android.features.call.api.CurrentCallService import kotlinx.coroutines.flow.MutableStateFlow -class FakeCurrentCallObserver( +class FakeCurrentCallService( initialValue: CurrentCall = CurrentCall.None, -) : CurrentCallObserver { +) : CurrentCallService { override val currentCall = MutableStateFlow(initialValue) fun setCurrentCall(value: CurrentCall) { diff --git a/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt b/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt index b6eb10b342..57ac545c19 100644 --- a/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt +++ b/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt @@ -13,7 +13,7 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import io.element.android.features.call.api.CurrentCall -import io.element.android.features.call.api.CurrentCallObserver +import io.element.android.features.call.api.CurrentCallService import io.element.android.features.roomcall.api.RoomCallState import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.room.MatrixRoom @@ -22,7 +22,7 @@ import javax.inject.Inject class RoomCallStatePresenter @Inject constructor( private val room: MatrixRoom, - private val currentCallObserver: CurrentCallObserver, + private val currentCallService: CurrentCallService, ) : Presenter { @Composable override fun present(): RoomCallState { @@ -34,7 +34,7 @@ class RoomCallStatePresenter @Inject constructor( room.sessionId in roomInfo?.activeRoomCallParticipants.orEmpty() } } - val currentCall by currentCallObserver.currentCall.collectAsState() + val currentCall by currentCallService.currentCall.collectAsState() val isUserLocallyInTheCall by remember { derivedStateOf { (currentCall as? CurrentCall.RoomCall)?.roomId == room.roomId diff --git a/features/roomcall/impl/src/test/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenterTest.kt b/features/roomcall/impl/src/test/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenterTest.kt index aa4a0eb676..ba0b425ec5 100644 --- a/features/roomcall/impl/src/test/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenterTest.kt +++ b/features/roomcall/impl/src/test/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenterTest.kt @@ -9,8 +9,8 @@ package io.element.android.features.roomcall.impl import com.google.common.truth.Truth.assertThat import io.element.android.features.call.api.CurrentCall -import io.element.android.features.call.api.CurrentCallObserver -import io.element.android.features.call.test.FakeCurrentCallObserver +import io.element.android.features.call.api.CurrentCallService +import io.element.android.features.call.test.FakeCurrentCallService import io.element.android.features.roomcall.api.RoomCallState import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom @@ -112,7 +112,7 @@ class RoomCallStatePresenterTest { } val presenter = createRoomCallStatePresenter( matrixRoom = room, - currentCallObserver = FakeCurrentCallObserver(initialValue = CurrentCall.RoomCall(room.roomId)), + currentCallService = FakeCurrentCallService(initialValue = CurrentCall.RoomCall(room.roomId)), ) presenter.test { skipItems(1) @@ -138,10 +138,10 @@ class RoomCallStatePresenterTest { ) ) } - val currentCallObserver = FakeCurrentCallObserver(initialValue = CurrentCall.RoomCall(room.roomId)) + val currentCallService = FakeCurrentCallService(initialValue = CurrentCall.RoomCall(room.roomId)) val presenter = createRoomCallStatePresenter( matrixRoom = room, - currentCallObserver = currentCallObserver + currentCallService = currentCallService ) presenter.test { skipItems(1) @@ -152,7 +152,7 @@ class RoomCallStatePresenterTest { isUserLocallyInTheCall = true, ) ) - currentCallObserver.setCurrentCall(CurrentCall.None) + currentCallService.setCurrentCall(CurrentCall.None) assertThat(awaitItem()).isEqualTo( RoomCallState.OnGoing( canJoinCall = true, @@ -189,11 +189,11 @@ class RoomCallStatePresenterTest { private fun createRoomCallStatePresenter( matrixRoom: MatrixRoom, - currentCallObserver: CurrentCallObserver = FakeCurrentCallObserver(), + currentCallService: CurrentCallService = FakeCurrentCallService(), ): RoomCallStatePresenter { return RoomCallStatePresenter( room = matrixRoom, - currentCallObserver = currentCallObserver, + currentCallService = currentCallService, ) } } From db4b4d3fa4e3434e13a2c7af4ee7abb578304c5f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 6 Nov 2024 17:29:20 +0100 Subject: [PATCH 6/6] Provide MutableStateFlow in the constructor of the fake class. --- .../features/call/test/FakeCurrentCallService.kt | 10 ++-------- .../roomcall/impl/RoomCallStatePresenterTest.kt | 8 +++++--- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/features/call/test/src/main/kotlin/io/element/android/features/call/test/FakeCurrentCallService.kt b/features/call/test/src/main/kotlin/io/element/android/features/call/test/FakeCurrentCallService.kt index ff0a9e9096..b9efc70ece 100644 --- a/features/call/test/src/main/kotlin/io/element/android/features/call/test/FakeCurrentCallService.kt +++ b/features/call/test/src/main/kotlin/io/element/android/features/call/test/FakeCurrentCallService.kt @@ -12,11 +12,5 @@ import io.element.android.features.call.api.CurrentCallService import kotlinx.coroutines.flow.MutableStateFlow class FakeCurrentCallService( - initialValue: CurrentCall = CurrentCall.None, -) : CurrentCallService { - override val currentCall = MutableStateFlow(initialValue) - - fun setCurrentCall(value: CurrentCall) { - currentCall.value = value - } -} + override val currentCall: MutableStateFlow = MutableStateFlow(CurrentCall.None), +) : CurrentCallService diff --git a/features/roomcall/impl/src/test/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenterTest.kt b/features/roomcall/impl/src/test/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenterTest.kt index ba0b425ec5..d29bff08ff 100644 --- a/features/roomcall/impl/src/test/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenterTest.kt +++ b/features/roomcall/impl/src/test/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenterTest.kt @@ -16,6 +16,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.tests.testutils.test +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.Test @@ -112,7 +113,7 @@ class RoomCallStatePresenterTest { } val presenter = createRoomCallStatePresenter( matrixRoom = room, - currentCallService = FakeCurrentCallService(initialValue = CurrentCall.RoomCall(room.roomId)), + currentCallService = FakeCurrentCallService(MutableStateFlow(CurrentCall.RoomCall(room.roomId))), ) presenter.test { skipItems(1) @@ -138,7 +139,8 @@ class RoomCallStatePresenterTest { ) ) } - val currentCallService = FakeCurrentCallService(initialValue = CurrentCall.RoomCall(room.roomId)) + val currentCall = MutableStateFlow(CurrentCall.RoomCall(room.roomId)) + val currentCallService = FakeCurrentCallService(currentCall = currentCall) val presenter = createRoomCallStatePresenter( matrixRoom = room, currentCallService = currentCallService @@ -152,7 +154,7 @@ class RoomCallStatePresenterTest { isUserLocallyInTheCall = true, ) ) - currentCallService.setCurrentCall(CurrentCall.None) + currentCall.value = CurrentCall.None assertThat(awaitItem()).isEqualTo( RoomCallState.OnGoing( canJoinCall = true,