From 174a6cad0d8401e67d2516365a9a2efde06f009c Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Mon, 18 May 2026 19:01:11 +0200 Subject: [PATCH] Create a new room when inviting people in a DM (#6756) * Create a new room when inviting people to a DM * Improve screenshot tests * Update screenshots --------- Co-authored-by: ElementBot --- .../android/appnav/LoggedInFlowNode.kt | 4 +- .../room/joined/JoinedRoomLoadedFlowNode.kt | 4 +- .../FakeJoinedRoomLoadedFlowNodeCallback.kt | 2 +- .../invitepeople/api/InvitePeopleEvents.kt | 1 + .../invitepeople/api/InvitePeopleState.kt | 2 + .../api/InvitePeopleStateProvider.kt | 4 ++ .../impl/DefaultInvitePeoplePresenter.kt | 48 ++++++++++++++++++- .../impl/DefaultInvitePeopleState.kt | 2 + .../impl/DefaultInvitePeopleStateProvider.kt | 3 ++ .../invitepeople/impl/InvitePeopleView.kt | 3 +- .../impl/DefaultInvitePeoplePresenterTest.kt | 48 +++++++++++++++++++ .../roomdetails/api/RoomDetailsEntryPoint.kt | 2 +- .../roomdetails/impl/RoomDetailsFlowNode.kt | 15 +++++- .../impl/RoomDetailsStateProvider.kt | 1 + .../roomdetails/impl/RoomDetailsView.kt | 34 +++++++++---- .../impl/invite/RoomInviteMembersNode.kt | 26 ++++++++++ .../impl/DefaultRoomDetailsEntryPointTest.kt | 2 +- .../roomdetails/impl/RoomDetailsViewTest.kt | 21 ++++++++ ...roomdetails.impl_RoomDetailsDark_18_en.png | 4 +- ...roomdetails.impl_RoomDetailsDark_19_en.png | 4 +- ....roomdetails.impl_RoomDetailsDark_5_en.png | 4 +- ....roomdetails.impl_RoomDetailsDark_6_en.png | 4 +- ...res.roomdetails.impl_RoomDetails_18_en.png | 4 +- ...res.roomdetails.impl_RoomDetails_19_en.png | 4 +- ...ures.roomdetails.impl_RoomDetails_5_en.png | 4 +- ...ures.roomdetails.impl_RoomDetails_6_en.png | 4 +- 26 files changed, 220 insertions(+), 34 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index f2120038a6..47e5b7c5b3 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -382,9 +382,9 @@ class LoggedInFlowNode( } is NavTarget.Room -> { val joinedRoomCallback = object : JoinedRoomLoadedFlowNode.Callback { - override fun navigateToRoom(roomId: RoomId, serverNames: List) { + override fun navigateToRoom(roomId: RoomId, serverNames: List, clearBackStack: Boolean) { lifecycleScope.launch { - attachRoom(roomIdOrAlias = roomId.toRoomIdOrAlias(), serverNames = serverNames, clearBackstack = false) + attachRoom(roomIdOrAlias = roomId.toRoomIdOrAlias(), serverNames = serverNames, clearBackstack = clearBackStack) } } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt index febd15e9c2..a8e5921973 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt @@ -82,7 +82,7 @@ class JoinedRoomLoadedFlowNode( plugins = plugins, ), DependencyInjectionGraphOwner { interface Callback : Plugin { - fun navigateToRoom(roomId: RoomId, serverNames: List) + fun navigateToRoom(roomId: RoomId, serverNames: List, clearBackStack: Boolean = false) fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) fun navigateToGlobalNotificationSettings() fun navigateToDeveloperSettings() @@ -150,7 +150,7 @@ class JoinedRoomLoadedFlowNode( callback.navigateToDeveloperSettings() } - override fun navigateToRoom(roomId: RoomId, serverNames: List) { + override fun navigateToRoom(roomId: RoomId, serverNames: List, clearBackStack: Boolean) { callback.navigateToRoom(roomId, serverNames) } diff --git a/appnav/src/test/kotlin/io/element/android/appnav/room/joined/FakeJoinedRoomLoadedFlowNodeCallback.kt b/appnav/src/test/kotlin/io/element/android/appnav/room/joined/FakeJoinedRoomLoadedFlowNodeCallback.kt index 2f17071870..b0669148dd 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/room/joined/FakeJoinedRoomLoadedFlowNodeCallback.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/room/joined/FakeJoinedRoomLoadedFlowNodeCallback.kt @@ -13,7 +13,7 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.tests.testutils.lambda.lambdaError class FakeJoinedRoomLoadedFlowNodeCallback : JoinedRoomLoadedFlowNode.Callback { - override fun navigateToRoom(roomId: RoomId, serverNames: List) = lambdaError() + override fun navigateToRoom(roomId: RoomId, serverNames: List, clearBackStack: Boolean) = lambdaError() override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) = lambdaError() override fun navigateToGlobalNotificationSettings() = lambdaError() override fun navigateToDeveloperSettings() = lambdaError() diff --git a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleEvents.kt b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleEvents.kt index 264aafd570..0422fac4f1 100644 --- a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleEvents.kt +++ b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleEvents.kt @@ -11,4 +11,5 @@ package io.element.android.features.invitepeople.api interface InvitePeopleEvents { data object SendInvites : InvitePeopleEvents data object CloseSearch : InvitePeopleEvents + data object ClearError : InvitePeopleEvents } diff --git a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleState.kt b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleState.kt index 9d342d191f..d14042cff7 100644 --- a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleState.kt +++ b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleState.kt @@ -9,10 +9,12 @@ package io.element.android.features.invitepeople.api import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.core.RoomId interface InvitePeopleState { val canInvite: Boolean val isSearchActive: Boolean val sendInvitesAction: AsyncAction + val createRoomFromDmAction: AsyncAction val eventSink: (InvitePeopleEvents) -> Unit } diff --git a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleStateProvider.kt b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleStateProvider.kt index ce30bcc1f6..b233ed07ef 100644 --- a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleStateProvider.kt +++ b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleStateProvider.kt @@ -10,6 +10,7 @@ package io.element.android.features.invitepeople.api import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.core.RoomId class InvitePeopleStateProvider : PreviewParameterProvider { override val values: Sequence @@ -25,6 +26,7 @@ private data class PreviewInvitePeopleState( override val canInvite: Boolean, override val isSearchActive: Boolean, override val sendInvitesAction: AsyncAction, + override val createRoomFromDmAction: AsyncAction, override val eventSink: (InvitePeopleEvents) -> Unit, ) : InvitePeopleState @@ -32,10 +34,12 @@ private fun aPreviewInvitePeopleState( canInvite: Boolean = false, isSearchActive: Boolean = false, sendInvitesAction: AsyncAction = AsyncAction.Uninitialized, + createRoomFromDmAction: AsyncAction = AsyncAction.Uninitialized, eventSink: (InvitePeopleEvents) -> Unit = {}, ) = PreviewInvitePeopleState( canInvite = canInvite, isSearchActive = isSearchActive, sendInvitesAction = sendInvitesAction, + createRoomFromDmAction = createRoomFromDmAction, eventSink = eventSink ) diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenter.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenter.kt index b223d30617..44627daca3 100644 --- a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenter.kt +++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenter.kt @@ -38,12 +38,17 @@ import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters +import io.element.android.libraries.matrix.api.createroom.RoomPreset import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.room.filterMembers +import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility +import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.api.room.recent.getRecentDirectRooms +import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.usersearch.api.UserRepository @@ -88,6 +93,7 @@ class DefaultInvitePeoplePresenter( var searchActive by rememberSaveable { mutableStateOf(false) } val showSearchLoader = rememberSaveable { mutableStateOf(false) } val sendInvitesAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } + val createRoomFromDmAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } val recentDirectRooms by produceState(emptyList(), roomMembers.value) { if (roomMembers.value.isSuccess()) { @@ -208,7 +214,13 @@ class DefaultInvitePeoplePresenter( ) } else { room.dataOrNull()?.let { - sessionCoroutineScope.sendInvites(it, selectedUsers.value, sendInvitesAction) + sessionCoroutineScope.launch { + if (it.isDm()) { + createRoomFromDm(it, selectedUsers.value, createRoomFromDmAction) + } else { + sendInvites(it, selectedUsers.value, sendInvitesAction) + } + } } } } @@ -216,6 +228,10 @@ class DefaultInvitePeoplePresenter( searchActive = false queryState.clearText() } + is InvitePeopleEvents.ClearError -> { + sendInvitesAction.value = AsyncAction.Uninitialized + createRoomFromDmAction.value = AsyncAction.Uninitialized + } } } @@ -228,6 +244,7 @@ class DefaultInvitePeoplePresenter( searchResults = searchResults.value, showSearchLoader = showSearchLoader.value, sendInvitesAction = sendInvitesAction.value, + createRoomFromDmAction = createRoomFromDmAction.value, suggestions = suggestions, eventSink = ::handleEvent, ) @@ -254,6 +271,35 @@ class DefaultInvitePeoplePresenter( } } + private fun CoroutineScope.createRoomFromDm( + currentRoom: JoinedRoom, + selectedUsers: List, + createRoomFromDmAction: MutableState>, + ) = launch { + createRoomFromDmAction.runUpdatingState { + val currentUsers = currentRoom.getMembers(limit = 100).getOrNull().orEmpty() + .filter { it.membership.isActive() } + val invitees = (currentUsers.map { it.userId } + selectedUsers.map { it.userId }) + .filter { it != matrixClient.sessionId } + .distinct() + matrixClient.createRoom( + CreateRoomParameters( + name = null, + topic = null, + isEncrypted = true, + isDirect = false, + visibility = RoomVisibility.Private, + preset = RoomPreset.PRIVATE_CHAT, + invite = invitees, + avatar = null, + joinRuleOverride = JoinRule.Invite, + historyVisibilityOverride = RoomHistoryVisibility.Invited, + isSpace = false, + ) + ) + } + } + @JvmName("toggleUserInSelectedUsers") private fun MutableState>.toggleUser(user: MatrixUser) { value = if (value.contains(user)) { diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleState.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleState.kt index 842bcf1148..46e5d9f1a5 100644 --- a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleState.kt +++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleState.kt @@ -14,6 +14,7 @@ import io.element.android.features.invitepeople.api.InvitePeopleState import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.user.MatrixUser import kotlinx.collections.immutable.ImmutableList @@ -26,6 +27,7 @@ data class DefaultInvitePeopleState( val selectedUsers: ImmutableList, override val isSearchActive: Boolean, override val sendInvitesAction: AsyncAction, + override val createRoomFromDmAction: AsyncAction, val suggestions: ImmutableList, override val eventSink: (InvitePeopleEvents) -> Unit ) : InvitePeopleState diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStateProvider.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStateProvider.kt index 45b47fbc6e..93a6e03bd3 100644 --- a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStateProvider.kt +++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStateProvider.kt @@ -18,6 +18,7 @@ import io.element.android.libraries.designsystem.preview.USER_NAME_CAROL import io.element.android.libraries.designsystem.preview.USER_NAME_EVE import io.element.android.libraries.designsystem.preview.USER_NAME_JUSTIN import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.matrix.ui.components.aMatrixUserList @@ -119,6 +120,7 @@ private fun aDefaultInvitePeopleState( isSearchActive: Boolean = false, showSearchLoader: Boolean = false, sendInvitesAction: AsyncAction = AsyncAction.Uninitialized, + createRoomFromDmAction: AsyncAction = AsyncAction.Uninitialized, suggestions: List = aMatrixUserList() .take(5) .map { user -> anInvitableUser(matrixUser = user, isSelected = user in selectedUsers) }, @@ -132,6 +134,7 @@ private fun aDefaultInvitePeopleState( isSearchActive = isSearchActive, showSearchLoader = showSearchLoader, sendInvitesAction = sendInvitesAction, + createRoomFromDmAction = createRoomFromDmAction, suggestions = suggestions.toImmutableList(), eventSink = {}, ) diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/InvitePeopleView.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/InvitePeopleView.kt index 3e9de70165..2bbd64c977 100644 --- a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/InvitePeopleView.kt +++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/InvitePeopleView.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -105,7 +106,7 @@ private fun InvitePeopleContentView( } InvitePeopleSearchBar( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.imePadding().fillMaxWidth(), queryState = state.searchQuery, showLoader = state.showSearchLoader, selectedUsers = state.selectedUsers, diff --git a/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt b/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt index 5d5a533bb6..4e141b2c4d 100644 --- a/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt +++ b/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt @@ -831,6 +831,54 @@ internal class DefaultInvitePeoplePresenterTest { } } + @Test + fun `present - inviting someone to a DM creates a new room`() = runTest { + val alice = aMatrixUser("@alice:example.com") + + val matrixClient = FakeMatrixClient( + encryptionService = FakeEncryptionService( + getUserIdentityResult = lambdaRecorder { userId: UserId -> + Result.success(IdentityState.Pinned) + } + ) + ) + val presenter = createDefaultInvitePeoplePresenter( + coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), + matrixClient = matrixClient, + joinedRoom = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + initialRoomInfo = aRoomInfo(isDm = true), + getMembersResult = { Result.success(listOf(aRoomMember(userId = alice.userId, membership = RoomMembershipState.JOIN))) }, + ) + ) + ) + presenter.test { + val initialState = awaitItem() + skipItems(1) + + // We want to add a new user to a DM + initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(alice)) + + // And we send the invites + initialState.eventSink(InvitePeopleEvents.SendInvites) + + skipItems(1) + + awaitItemAsDefault().run { + assertThat(canInvite).isTrue() + assertThat(sendInvitesAction.isUninitialized()).isTrue() + // Inviting to a DM should trigger the creation of a new room + assertThat(createRoomFromDmAction.isLoading()).isTrue() + } + + awaitItemAsDefault().run { + assertThat(sendInvitesAction.isUninitialized()).isTrue() + // Once the room is created, the action should be successful + assertThat(createRoomFromDmAction.isSuccess()).isTrue() + } + } + } + private suspend fun FakeUserRepository.emitStateWithUsers( users: List, isSearching: Boolean = false diff --git a/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt b/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt index 07e10c65ef..bbaac0cbc1 100644 --- a/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt +++ b/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt @@ -40,7 +40,7 @@ interface RoomDetailsEntryPoint : FeatureEntryPoint { interface Callback : Plugin { fun navigateToGlobalNotificationSettings() fun navigateToDeveloperSettings() - fun navigateToRoom(roomId: RoomId, serverNames: List) + fun navigateToRoom(roomId: RoomId, serverNames: List, clearBackStack: Boolean = false) fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) fun startForwardEventFlow(eventId: EventId, fromPinnedEvents: Boolean) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt index 818287ab68..4cf5056b4e 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt @@ -263,7 +263,20 @@ class RoomDetailsFlowNode( } NavTarget.InviteMembers -> { - createNode(buildContext) + val callback = object : RoomInviteMembersNode.Callback { + override fun openCreatedRoom(roomId: RoomId) { + navigateUp() + room.roomCoroutineScope.launch { + callback.navigateToRoom( + roomId = roomId, + serverNames = emptyList(), + // Remove the invite screen from the backstack to avoid navigating back to it after the new room has been created + clearBackStack = true, + ) + } + } + } + createNode(buildContext, plugins = listOf(callback)) } is NavTarget.RoomNotificationSettings -> { 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 ad627d8677..b569527d9c 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 @@ -180,6 +180,7 @@ fun aDmRoomDetailsState( roomName = roomName, isPublic = false, isEncrypted = isEncrypted, + canInvite = true, roomType = RoomDetailsType.Dm(otherMember = aDmRoomMember(isIgnored = isDmMemberIgnored)), roomMemberDetailsState = aUserProfileState( isBlocked = AsyncData.Success(isDmMemberIgnored), 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 e877c24554..cf5265b145 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 @@ -208,8 +208,15 @@ fun RoomDetailsView( onClick = onSecurityAndPrivacyClick ) } + } - state.roomMemberDetailsState?.let { dmMemberDetails -> + state.roomMemberDetailsState?.let { dmMemberDetails -> + if (state.canInvite) { + PreferenceCategory { + InviteItem(onClick = invitePeople) + } + } + PreferenceCategory { ProfileItem( verificationState = dmMemberDetails.verificationState, onClick = { onProfileClick(dmMemberDetails.userId) } @@ -374,14 +381,14 @@ private fun MainActionsSection( onClick = { onCall(CallIntent.VIDEO) }, ) } + if (state.canInvite && state.roomType !is RoomDetailsType.Dm) { + MainActionButton( + title = stringResource(CommonStrings.action_invite), + imageVector = CompoundIcons.UserAdd(), + onClick = onInvitePeople, + ) + } if (state.roomType is RoomDetailsType.Room) { - if (state.canInvite) { - MainActionButton( - title = stringResource(CommonStrings.action_invite), - imageVector = CompoundIcons.UserAdd(), - onClick = onInvitePeople, - ) - } // Share CTA should be hidden for DMs MainActionButton( title = stringResource(CommonStrings.action_share), @@ -693,6 +700,17 @@ private fun MembersItem( ) } +@Composable +private fun InviteItem( + onClick: () -> Unit, +) { + ListItem( + headlineContent = { Text(stringResource(R.string.screen_room_details_invite_title)) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.UserAdd())), + onClick = onClick, + ) +} + @Composable private fun PinnedMessagesItem( pinnedMessagesCount: Int?, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersNode.kt index ea0ed1bb72..3919817313 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersNode.kt @@ -11,6 +11,7 @@ package io.element.android.features.roomdetails.impl.invite import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node @@ -19,10 +20,16 @@ import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.MobileScreen import io.element.android.annotations.ContributesNode +import io.element.android.features.invitepeople.api.InvitePeopleEvents import io.element.android.features.invitepeople.api.InvitePeoplePresenter import io.element.android.features.invitepeople.api.InvitePeopleRenderer +import io.element.android.libraries.architecture.callback +import io.element.android.libraries.designsystem.components.ProgressDialog +import io.element.android.libraries.designsystem.components.async.AsyncActionView import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.JoinedRoom +import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.services.analytics.api.AnalyticsService @ContributesNode(RoomScope::class) @@ -35,6 +42,10 @@ class RoomInviteMembersNode( room: JoinedRoom, invitePeoplePresenterFactory: InvitePeoplePresenter.Factory, ) : Node(buildContext, plugins = plugins) { + interface Callback : Plugin { + fun openCreatedRoom(roomId: RoomId) + } + init { lifecycle.subscribe( onResume = { @@ -48,6 +59,8 @@ class RoomInviteMembersNode( roomId = room.roomId, ) + private val callback = plugins.callback() + @Composable override fun View(modifier: Modifier) { val state = invitePeoplePresenter.present() @@ -59,6 +72,19 @@ class RoomInviteMembersNode( } } + AsyncActionView( + async = state.createRoomFromDmAction, + onSuccess = { roomId -> + callback.openCreatedRoom(roomId) + }, + progressDialog = { + ProgressDialog(text = stringResource(CommonStrings.common_creating_room)) + }, + onErrorDismiss = { + state.eventSink(InvitePeopleEvents.ClearError) + } + ) + RoomInviteMembersView( state = state, modifier = modifier, diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPointTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPointTest.kt index bcf25b2aac..597cc82f7d 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPointTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPointTest.kt @@ -70,7 +70,7 @@ class DefaultRoomDetailsEntryPointTest { val callback = object : RoomDetailsEntryPoint.Callback { override fun navigateToGlobalNotificationSettings() = lambdaError() override fun navigateToDeveloperSettings() = lambdaError() - override fun navigateToRoom(roomId: RoomId, serverNames: List) = lambdaError() + override fun navigateToRoom(roomId: RoomId, serverNames: List, clearBackStack: Boolean) = lambdaError() override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) = lambdaError() override fun startForwardEventFlow(eventId: EventId, fromPinnedEvents: Boolean) = lambdaError() } diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt index 5fe0130c73..9494df5d44 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt @@ -13,6 +13,8 @@ package io.element.android.features.roomdetails.impl import androidx.activity.ComponentActivity import androidx.compose.ui.test.AndroidComposeUiTest import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.onAllNodesWithText +import androidx.compose.ui.test.onLast import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick @@ -339,6 +341,25 @@ class RoomDetailsViewTest { clickOn(R.string.screen_room_details_profile_row_title) } } + + @Config(qualifiers = "h1024dp") + @Test + fun `click on invite invokes the expected callback`() = runAndroidComposeUiTest { + ensureCalledOnce { callback -> + setRoomDetailView( + state = aRoomDetailsState( + eventSink = EventsRecorder(expectEvents = false), + roomType = RoomDetailsType.Dm( + aDmRoomMember(userId = UserId("@other:local.org")), + ), + roomMemberDetailsState = aUserProfileState(userId = A_USER_ID), + canInvite = true, + ), + invitePeople = callback, + ) + onAllNodesWithText(activity!!.getString(R.string.screen_room_details_invite_title)).onLast().performClick() + } + } } private fun AndroidComposeUiTest.setRoomDetailView( diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_18_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_18_en.png index ddcd78552d..9f56dab8f3 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_18_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_18_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7b9375965be172b7a1a6b00be38c66dc5f73d7d76f6b07fd1cb8defbadae840 -size 41554 +oid sha256:2f239cb428d2e4cffa86e8d8397904af0c0c5e621585f31bcf3135cdd2c81a40 +size 40541 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_19_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_19_en.png index 669af01562..81bb7961e9 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_19_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_19_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7890cc2fc8e722bfdee5e1c0cdda335c386a2e70a918b710a44a788457dd8497 -size 41507 +oid sha256:3bcc557108fc16a1f63d22b3512d271d30dccc801838607e78921baf9ba490c0 +size 40509 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_5_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_5_en.png index 31daf341c2..2d3d2f0a74 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:105399c3ca12de9e911ccf7b4aaebe87bffeba45b1740d067b5b8e67c5647952 -size 41196 +oid sha256:76f63f6c98318762a574d7fb04c8e23ad8312b1fc9adc404669ac6d9e2c42097 +size 40127 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_6_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_6_en.png index 3309d53220..bca90757f7 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5da1fef365731946e08a38250187fe6f3108aff466d7771863c0ba52fb5b7728 -size 42254 +oid sha256:3063251e98ea6e797e5178cb954d99e71fc1422df19c24a59b1395531380e941 +size 41113 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_18_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_18_en.png index 74405c8661..3151e8d088 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_18_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_18_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fef392cb892bf6dc746ed59f1279bf9558536ef439715d1834bd88260739949d -size 42441 +oid sha256:6fb58a8db96c0e76d5a0e8c8d2a38ae206e3dade479bd924fd404902c8e69b42 +size 41322 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_19_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_19_en.png index dd6c6a993d..e213e801f3 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_19_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_19_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c9b7c26c5f0638dbe7731e239fc16bb2ba4330986af7218688a3a12ede2bfd9d -size 42313 +oid sha256:5030731135dec08034e1992499caa251aae8039cebafb09210fc83d622b06bd9 +size 41257 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_5_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_5_en.png index e6cae695cf..5c16501c76 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8d6a5102a9dd44a6a15dc6b40ec422a115a302506a2b3808a1df84ca34cb323e -size 41972 +oid sha256:e54b946de1128b1109ece7553e73d7676199edc45e7f275014c37503336d302e +size 40862 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_6_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_6_en.png index a1947b9c2c..55ab231ede 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1538aa1f1313df1f6cbbc0ed7ef92d264c85b7de0080492a7cba19bbd235131d -size 43100 +oid sha256:4f3df6d78498d2b390cf926fb1111fdfa0f38ba5dd9e023b9bd2af42c3951511 +size 41921