diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomLoadedFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomLoadedFlowNode.kt index 21e412ef638..b9a537dc241 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomLoadedFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomLoadedFlowNode.kt @@ -40,6 +40,7 @@ import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.DaggerComponentOwner import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom @@ -61,6 +62,7 @@ class RoomLoadedFlowNode @AssistedInject constructor( private val roomDetailsEntryPoint: RoomDetailsEntryPoint, private val appNavigationStateService: AppNavigationStateService, private val appCoroutineScope: CoroutineScope, + private val matrixClient: MatrixClient, roomComponentFactory: RoomComponentFactory, roomMembershipObserver: RoomMembershipObserver, ) : BaseFlowNode( @@ -92,6 +94,7 @@ class RoomLoadedFlowNode @AssistedInject constructor( Timber.v("OnCreate => ${inputs.room.roomId}") appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId) fetchRoomMembers() + trackVisitedRoom() }, onResume = { appCoroutineScope.launch { @@ -117,6 +120,10 @@ class RoomLoadedFlowNode @AssistedInject constructor( inputs() } + private fun trackVisitedRoom() = lifecycleScope.launch { + matrixClient.trackRecentlyVisitedRoom(inputs.room.roomId) + } + private fun fetchRoomMembers() = lifecycleScope.launch { inputs.room.updateMembers() } diff --git a/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt index ecc6a5dad04..0595ebdbea0 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt @@ -33,6 +33,7 @@ import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint import io.element.android.libraries.architecture.childNode import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMembershipObserver +import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.services.appnavstate.test.FakeAppNavigationStateService import kotlinx.coroutines.CoroutineScope @@ -101,6 +102,7 @@ class RoomFlowNodeTest { roomMembershipObserver = RoomMembershipObserver(), appCoroutineScope = coroutineScope, roomComponentFactory = FakeRoomComponentFactory(), + matrixClient = FakeMatrixClient(), ) @Test diff --git a/changelog.d/2634.misc b/changelog.d/2634.misc new file mode 100644 index 00000000000..0ec68a21153 --- /dev/null +++ b/changelog.d/2634.misc @@ -0,0 +1 @@ +Show users from last visited DM as suggestion when starting a Chat or when creating a Room. diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleUserListStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleUserListStateProvider.kt index 4fb1149a013..63e24171137 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleUserListStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleUserListStateProvider.kt @@ -19,6 +19,7 @@ package io.element.android.features.createroom.impl.addpeople import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.createroom.impl.userlist.SelectionMode import io.element.android.features.createroom.impl.userlist.UserListState +import io.element.android.features.createroom.impl.userlist.aRecentDirectRoomList import io.element.android.features.createroom.impl.userlist.aUserListState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.ui.components.aMatrixUserList @@ -29,13 +30,13 @@ open class AddPeopleUserListStateProvider : PreviewParameterProvider get() = sequenceOf( aUserListState(), - aUserListState().copy( + aUserListState( searchResults = SearchBarResultState.Results(aMatrixUserList().toImmutableList()), selectedUsers = aMatrixUserList().toImmutableList(), isSearchActive = false, selectionMode = SelectionMode.Multiple, ), - aUserListState().copy( + aUserListState( searchResults = SearchBarResultState.Results( aMatrixUserList() .mapIndexed { index, matrixUser -> @@ -46,6 +47,9 @@ open class AddPeopleUserListStateProvider : PreviewParameterProvider - Column( + UserListView( modifier = Modifier .fillMaxSize() .padding(padding) .consumeWindowInsets(padding), - ) { - UserListView( - modifier = Modifier - .fillMaxWidth(), - state = state, - showBackButton = false, - onUserSelected = { }, - onUserDeselected = {}, - ) - } + state = state, + showBackButton = false, + onUserSelected = {}, + onUserDeselected = {}, + ) } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/UserListView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/UserListView.kt index 3cc009989ab..0e1c4480158 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/UserListView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/UserListView.kt @@ -19,17 +19,27 @@ package io.element.android.features.createroom.impl.components import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyColumn 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.features.createroom.impl.userlist.UserListEvents import io.element.android.features.createroom.impl.userlist.UserListState import io.element.android.features.createroom.impl.userlist.UserListStateProvider +import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.HorizontalDivider +import io.element.android.libraries.designsystem.theme.components.ListSectionHeader import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.ui.components.CheckableUserRow +import io.element.android.libraries.matrix.ui.components.CheckableUserRowData import io.element.android.libraries.matrix.ui.components.SelectedUsersRowList +import io.element.android.libraries.matrix.ui.model.getAvatarData +import io.element.android.libraries.matrix.ui.model.getBestName +import io.element.android.libraries.ui.strings.CommonStrings @Composable fun UserListView( @@ -74,6 +84,43 @@ fun UserListView( }, ) } + if (!state.isSearchActive && state.recentDirectRooms.isNotEmpty()) { + LazyColumn { + item { + ListSectionHeader( + title = stringResource(id = CommonStrings.common_suggestions), + hasDivider = false, + ) + } + state.recentDirectRooms.forEachIndexed { index, recentDirectRoom -> + item { + val isSelected = state.selectedUsers.any { + recentDirectRoom.matrixUser.userId == it.userId + } + CheckableUserRow( + checked = isSelected, + onCheckedChange = { + if (isSelected) { + state.eventSink(UserListEvents.RemoveFromSelection(recentDirectRoom.matrixUser)) + onUserDeselected(recentDirectRoom.matrixUser) + } else { + state.eventSink(UserListEvents.AddToSelection(recentDirectRoom.matrixUser)) + onUserSelected(recentDirectRoom.matrixUser) + } + }, + data = CheckableUserRowData.Resolved( + avatarData = recentDirectRoom.matrixUser.getAvatarData(AvatarSize.UserListItem), + name = recentDirectRoom.matrixUser.getBestName(), + subtext = recentDirectRoom.matrixUser.userId.value, + ), + ) + if (index < state.recentDirectRooms.lastIndex) { + HorizontalDivider() + } + } + } + } + } } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt index 723c6507931..0638d8abbbc 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt @@ -17,9 +17,12 @@ package io.element.android.features.createroom.impl.root import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.createroom.impl.userlist.UserListState +import io.element.android.features.createroom.impl.userlist.aRecentDirectRoomList import io.element.android.features.createroom.impl.userlist.aUserListState import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.usersearch.api.UserSearchResult import kotlinx.collections.immutable.persistentListOf @@ -28,7 +31,7 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider get() = sequenceOf( aCreateRoomRootState(), - aCreateRoomRootState().copy( + aCreateRoomRootState( startDmAction = AsyncAction.Loading, userListState = aMatrixUser().let { aUserListState().copy( @@ -39,7 +42,7 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider = AsyncAction.Uninitialized, + eventSink: (CreateRoomRootEvents) -> Unit = {}, +) = CreateRoomRootState( + applicationName = applicationName, + userListState = userListState, + startDmAction = startDmAction, + eventSink = eventSink, ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt index 4f874c57ddc..33707896faa 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt @@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable @@ -46,11 +47,14 @@ import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.aliasScreenTitle import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.ListSectionHeader 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 import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.ui.components.MatrixUserRow import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.persistentListOf @Composable fun CreateRoomRootView( @@ -77,7 +81,11 @@ fun CreateRoomRootView( ) { UserListView( modifier = Modifier.fillMaxWidth(), - state = state.userListState, + // Do not render suggestions in this case, the suggestion will be rendered + // by CreateRoomActionButtonsList + state = state.userListState.copy( + recentDirectRooms = persistentListOf(), + ), onUserSelected = { state.eventSink(CreateRoomRootEvents.StartDM(it)) }, @@ -89,6 +97,7 @@ fun CreateRoomRootView( state = state, onNewRoomClicked = onNewRoomClicked, onInvitePeopleClicked = onInviteFriendsClicked, + onDmClicked = onOpenDM, ) } } @@ -106,7 +115,7 @@ fun CreateRoomRootView( onRetry = { state.userListState.selectedUsers.firstOrNull() ?.let { state.eventSink(CreateRoomRootEvents.StartDM(it)) } - // Cancel start DM if there is no more selected user (should not happen) + // Cancel start DM if there is no more selected user (should not happen) ?: state.eventSink(CreateRoomRootEvents.CancelStartDM) }, onErrorDismiss = { state.eventSink(CreateRoomRootEvents.CancelStartDM) }, @@ -139,18 +148,43 @@ private fun CreateRoomActionButtonsList( state: CreateRoomRootState, onNewRoomClicked: () -> Unit, onInvitePeopleClicked: () -> Unit, + onDmClicked: (RoomId) -> Unit, ) { - Column { - CreateRoomActionButton( - iconRes = CompoundDrawables.ic_compound_plus, - text = stringResource(id = R.string.screen_create_room_action_create_room), - onClick = onNewRoomClicked, - ) - CreateRoomActionButton( - iconRes = CompoundDrawables.ic_compound_share_android, - text = stringResource(id = CommonStrings.action_invite_friends_to_app, state.applicationName), - onClick = onInvitePeopleClicked, - ) + LazyColumn { + item { + CreateRoomActionButton( + iconRes = CompoundDrawables.ic_compound_plus, + text = stringResource(id = R.string.screen_create_room_action_create_room), + onClick = onNewRoomClicked, + ) + } + item { + CreateRoomActionButton( + iconRes = CompoundDrawables.ic_compound_share_android, + text = stringResource(id = CommonStrings.action_invite_friends_to_app, state.applicationName), + onClick = onInvitePeopleClicked, + ) + } + if (state.userListState.recentDirectRooms.isNotEmpty()) { + item { + ListSectionHeader( + title = stringResource(id = CommonStrings.common_suggestions), + hasDivider = false, + ) + } + state.userListState.recentDirectRooms.forEach { recentDirectRoom -> + item { + MatrixUserRow( + modifier = Modifier.clickable( + onClick = { + onDmClicked(recentDirectRoom.roomId) + } + ), + matrixUser = recentDirectRoom.matrixUser, + ) + } + } + } } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenter.kt index a210a6debd4..31daf625134 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenter.kt @@ -30,6 +30,9 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.room.recent.RecentDirectRoom +import io.element.android.libraries.matrix.api.room.recent.getRecentDirectRooms import io.element.android.libraries.usersearch.api.UserRepository import io.element.android.libraries.usersearch.api.UserSearchResult import kotlinx.collections.immutable.ImmutableList @@ -41,6 +44,7 @@ class DefaultUserListPresenter @AssistedInject constructor( @Assisted val args: UserListPresenterArgs, @Assisted val userRepository: UserRepository, @Assisted val userListDataStore: UserListDataStore, + private val matrixClient: MatrixClient, ) : UserListPresenter { @AssistedFactory @ContributesBinding(SessionScope::class) @@ -54,6 +58,10 @@ class DefaultUserListPresenter @AssistedInject constructor( @Composable override fun present(): UserListState { + var recentDirectRooms by remember { mutableStateOf(emptyList()) } + LaunchedEffect(Unit) { + recentDirectRooms = matrixClient.getRecentDirectRooms() + } var isSearchActive by rememberSaveable { mutableStateOf(false) } val selectedUsers by userListDataStore.selectedUsers().collectAsState(emptyList()) var searchQuery by rememberSaveable { mutableStateOf("") } @@ -82,6 +90,7 @@ class DefaultUserListPresenter @AssistedInject constructor( isSearchActive = isSearchActive, showSearchLoader = showSearchLoader, selectionMode = args.selectionMode, + recentDirectRooms = recentDirectRooms.toImmutableList(), eventSink = { event -> when (event) { is UserListEvents.OnSearchActiveChanged -> isSearchActive = event.active diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListState.kt index a27191881e2..b7b61fbe520 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListState.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListState.kt @@ -17,6 +17,7 @@ package io.element.android.features.createroom.impl.userlist import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.matrix.api.room.recent.RecentDirectRoom import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.usersearch.api.UserSearchResult import kotlinx.collections.immutable.ImmutableList @@ -28,6 +29,7 @@ data class UserListState( val selectedUsers: ImmutableList, val isSearchActive: Boolean, val selectionMode: SelectionMode, + val recentDirectRooms: ImmutableList, val eventSink: (UserListEvents) -> Unit, ) { val isMultiSelectionEnabled = selectionMode == SelectionMode.Multiple diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListStateProvider.kt index 193fa7e71ff..fc46ae19535 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListStateProvider.kt @@ -18,54 +18,82 @@ package io.element.android.features.createroom.impl.userlist import androidx.compose.ui.tooling.preview.PreviewParameterProvider 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.room.recent.RecentDirectRoom +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.components.aMatrixUserList import io.element.android.libraries.usersearch.api.UserSearchResult -import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList open class UserListStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aUserListState(), - aUserListState().copy( + aUserListState( isSearchActive = false, selectedUsers = aListOfSelectedUsers(), selectionMode = SelectionMode.Multiple, ), - aUserListState().copy(isSearchActive = true), - aUserListState().copy(isSearchActive = true, searchQuery = "someone"), - aUserListState().copy(isSearchActive = true, searchQuery = "someone", selectionMode = SelectionMode.Multiple), - aUserListState().copy( + aUserListState(isSearchActive = true), + aUserListState(isSearchActive = true, searchQuery = "someone"), + aUserListState(isSearchActive = true, searchQuery = "someone", selectionMode = SelectionMode.Multiple), + aUserListState( isSearchActive = true, searchQuery = "@someone:matrix.org", selectedUsers = aMatrixUserList().toImmutableList(), searchResults = SearchBarResultState.Results(aListOfUserSearchResults()), ), - aUserListState().copy( + aUserListState( isSearchActive = true, searchQuery = "@someone:matrix.org", selectionMode = SelectionMode.Multiple, selectedUsers = aMatrixUserList().toImmutableList(), searchResults = SearchBarResultState.Results(aListOfUserSearchResults()), ), - aUserListState().copy( + aUserListState( isSearchActive = true, searchQuery = "something-with-no-results", searchResults = SearchBarResultState.NoResultsFound() ), - aUserListState().copy(isSearchActive = true, searchQuery = "someone", selectionMode = SelectionMode.Single), + aUserListState( + isSearchActive = true, + searchQuery = "someone", + selectionMode = SelectionMode.Single, + ), + aUserListState( + recentDirectRooms = aRecentDirectRoomList(), + ), ) } -fun aUserListState() = UserListState( - isSearchActive = false, - searchQuery = "", - searchResults = SearchBarResultState.Initial(), - selectedUsers = persistentListOf(), - selectionMode = SelectionMode.Single, - showSearchLoader = false, - eventSink = {} +fun aUserListState( + searchQuery: String = "", + isSearchActive: Boolean = false, + searchResults: SearchBarResultState> = SearchBarResultState.Initial(), + selectedUsers: List = emptyList(), + showSearchLoader: Boolean = false, + selectionMode: SelectionMode = SelectionMode.Single, + recentDirectRooms: List = emptyList(), + eventSink: (UserListEvents) -> Unit = {}, +) = UserListState( + searchQuery = searchQuery, + isSearchActive = isSearchActive, + searchResults = searchResults, + selectedUsers = selectedUsers.toImmutableList(), + showSearchLoader = showSearchLoader, + selectionMode = selectionMode, + recentDirectRooms = recentDirectRooms.toImmutableList(), + eventSink = eventSink ) fun aListOfSelectedUsers() = aMatrixUserList().take(6).toImmutableList() fun aListOfUserSearchResults() = aMatrixUserList().take(6).map { UserSearchResult(it) }.toImmutableList() + +fun aRecentDirectRoomList( + count: Int = 5 +): List = aMatrixUserList() + .take(count) + .map { + RecentDirectRoom(RoomId("!aRoom:id"), it) + } diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenterTests.kt index 579bd175f59..c4d6d9ab00c 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenterTests.kt @@ -21,6 +21,7 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.matrix.ui.components.aMatrixUserList import io.element.android.libraries.usersearch.api.UserSearchResult @@ -45,6 +46,7 @@ class DefaultUserListPresenterTests { UserListPresenterArgs(selectionMode = SelectionMode.Single), userRepository, UserListDataStore(), + FakeMatrixClient(), ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -66,6 +68,7 @@ class DefaultUserListPresenterTests { UserListPresenterArgs(selectionMode = SelectionMode.Multiple), userRepository, UserListDataStore(), + FakeMatrixClient(), ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -87,6 +90,7 @@ class DefaultUserListPresenterTests { UserListPresenterArgs(selectionMode = SelectionMode.Single), userRepository, UserListDataStore(), + FakeMatrixClient(), ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -123,6 +127,7 @@ class DefaultUserListPresenterTests { ), userRepository, UserListDataStore(), + FakeMatrixClient(), ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -175,6 +180,7 @@ class DefaultUserListPresenterTests { ), userRepository, UserListDataStore(), + FakeMatrixClient(), ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -200,6 +206,7 @@ class DefaultUserListPresenterTests { UserListPresenterArgs(selectionMode = SelectionMode.Single), userRepository, UserListDataStore(), + FakeMatrixClient(), ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index fef657571c9..07b5b310bce 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -82,6 +82,12 @@ interface MatrixRoom : Closeable { */ suspend fun updateMembers() + /** + * Get the members of the room. Note: generally this should not be used, please use + * [membersStateFlow] and [updateMembers] instead. + */ + suspend fun getMembers(limit: Int = 5): Result> + /** * Will return an updated member or an error. */ diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/recent/RecentDirectRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/recent/RecentDirectRoom.kt new file mode 100644 index 00000000000..c2fb147aa00 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/recent/RecentDirectRoom.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.api.room.recent + +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.CurrentUserMembership +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.toMatrixUser +import io.element.android.libraries.matrix.api.user.MatrixUser +import kotlinx.coroutines.flow.first + +private const val MAX_RECENT_DIRECT_ROOMS_TO_RETURN = 5 + +data class RecentDirectRoom( + val roomId: RoomId, + val matrixUser: MatrixUser, +) + +suspend fun MatrixClient.getRecentDirectRooms( + maxNumberOfResults: Int = MAX_RECENT_DIRECT_ROOMS_TO_RETURN, +): List { + val result = mutableListOf() + val foundUserIds = mutableSetOf() + getRecentlyVisitedRooms().getOrNull()?.let { roomIds -> + roomIds + .mapNotNull { roomId -> getRoom(roomId) } + .filter { it.isDm && it.isJoined() } + .map { room -> + val otherUser = room.getMembers().getOrNull() + ?.firstOrNull { it.userId != sessionId } + ?.takeIf { foundUserIds.add(it.userId) } + ?.toMatrixUser() + if (otherUser != null) { + result.add( + RecentDirectRoom(room.roomId, otherUser) + ) + // Return early to avoid useless computation + if (result.size >= maxNumberOfResults) { + return@map + } + } + } + } + return result +} + +suspend fun MatrixRoom.isJoined(): Boolean { + return roomInfoFlow.first().currentUserMembership == CurrentUserMembership.JOINED +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 91e25d2498c..0531c59d4a4 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -237,6 +237,16 @@ class RustMatrixRoom( roomMemberListFetcher.fetchRoomMembers(source = source) } + override suspend fun getMembers(limit: Int) = withContext(roomDispatcher) { + runCatching { + innerRoom.members().use { + it.nextChunk(limit.toUInt()).orEmpty().map { roomMember -> + RoomMemberMapper.map(roomMember) + } + } + } + } + override suspend fun getUpdatedMember(userId: UserId): Result = withContext(roomDispatcher) { runCatching { RoomMemberMapper.map(innerRoom.member(userId.value)) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index f2395241b5c..11582433d59 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -201,6 +201,10 @@ class FakeMatrixRoom( return getRoomMemberResult } + override suspend fun getMembers(limit: Int): Result> { + return Result.success(emptyList()) + } + override suspend fun updateRoomNotificationSettings(): Result = simulateLongTask { val notificationSettings = notificationSettingsService.getRoomNotificationSettings(roomId, isEncrypted, isOneToOne).getOrThrow() roomNotificationSettingsStateFlow.value = MatrixRoomNotificationSettingsState.Ready(notificationSettings)