From 2a35edb14aec3d409ff80b10d5703d1696954a1e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 5 Nov 2024 17:39:39 +0100 Subject: [PATCH] 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,