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 5ac3bc2824..bba6e80ebe 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 @@ -11,6 +11,7 @@ package io.element.android.features.invitepeople.impl import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState @@ -39,6 +40,7 @@ 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.recent.getRecentDirectRooms 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 @@ -47,11 +49,16 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +private const val MAX_SUGGESTIONS_COUNT = 5 + @AssistedInject class DefaultInvitePeoplePresenter( @Assisted private val joinedRoom: JoinedRoom?, @@ -78,6 +85,34 @@ class DefaultInvitePeoplePresenter( val showSearchLoader = rememberSaveable { mutableStateOf(false) } val sendInvitesAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } + val recentDirectRooms by produceState(emptyList(), roomMembers.value) { + if (roomMembers.value.isSuccess()) { + val activeMemberIds = roomMembers.value.dataOrNull().orEmpty() + .filter { it.membership.isActive() } + .mapTo(mutableSetOf()) { it.userId } + + value = matrixClient.getRecentDirectRooms() + .filterNot { it.matrixUser.userId in activeMemberIds } + .take(MAX_SUGGESTIONS_COUNT) + .toList() + } + } + + // Convert recent direct rooms to InvitableUser for display + val suggestions by remember { + derivedStateOf { + recentDirectRooms.map { recentDirectRoom -> + InvitableUser( + matrixUser = recentDirectRoom.matrixUser, + isSelected = recentDirectRoom.matrixUser in selectedUsers.value, + isAlreadyJoined = false, + isAlreadyInvited = false, + isUnresolved = false, + ) + }.toImmutableList() + } + } + val room by produceState(if (joinedRoom != null) AsyncData.Success(joinedRoom) else AsyncData.Loading()) { if (joinedRoom == null) { val result = matrixClient.getJoinedRoom(roomId) @@ -118,6 +153,7 @@ class DefaultInvitePeoplePresenter( is DefaultInvitePeopleEvents.ToggleUser -> { selectedUsers.toggleUser(event.user) searchResults.toggleUser(event.user) + // suggestions will automatically update via derivedStateOf when selectedUsers changes } is InvitePeopleEvents.SendInvites -> { room.dataOrNull()?.let { @@ -140,6 +176,7 @@ class DefaultInvitePeoplePresenter( searchResults = searchResults.value, showSearchLoader = showSearchLoader.value, sendInvitesAction = sendInvitesAction.value, + suggestions = suggestions, eventSink = ::handleEvent, ) } 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 917915e4d6..0b9f2740c0 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 @@ -25,5 +25,6 @@ data class DefaultInvitePeopleState( val selectedUsers: ImmutableList, override val isSearchActive: Boolean, override val sendInvitesAction: 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 2f28db8786..c274f877d5 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 @@ -101,6 +101,9 @@ private fun aDefaultInvitePeopleState( isSearchActive: Boolean = false, showSearchLoader: Boolean = false, sendInvitesAction: AsyncAction = AsyncAction.Uninitialized, + suggestions: List = aMatrixUserList() + .take(5) + .map { user -> anInvitableUser(matrixUser = user, isSelected = user in selectedUsers) }, ): DefaultInvitePeopleState { return DefaultInvitePeopleState( room = room, @@ -111,6 +114,7 @@ private fun aDefaultInvitePeopleState( isSearchActive = isSearchActive, showSearchLoader = showSearchLoader, sendInvitesAction = sendInvitesAction, + 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 a8e056c7aa..8e11f42655 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 @@ -31,6 +31,8 @@ import io.element.android.libraries.designsystem.components.async.AsyncLoading 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.designsystem.theme.components.SearchBar import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.designsystem.theme.components.Text @@ -82,6 +84,10 @@ private fun InvitePeopleContentView( modifier = modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(16.dp), ) { + fun toggleUser(user: MatrixUser) { + state.eventSink(DefaultInvitePeopleEvents.ToggleUser(user)) + } + InvitePeopleSearchBar( modifier = Modifier.fillMaxWidth(), query = state.searchQuery, @@ -97,17 +103,45 @@ private fun InvitePeopleContentView( ) }, onTextChange = { state.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery(it)) }, - onToggleUser = { state.eventSink(DefaultInvitePeopleEvents.ToggleUser(it)) }, + onToggleUser = ::toggleUser, ) if (!state.isSearchActive) { - SelectedUsersRowList( - modifier = Modifier.fillMaxWidth(), - selectedUsers = state.selectedUsers, - autoScroll = true, - onUserRemove = { state.eventSink(DefaultInvitePeopleEvents.ToggleUser(it)) }, - contentPadding = PaddingValues(16.dp), - ) + if (state.selectedUsers.isNotEmpty()) { + SelectedUsersRowList( + modifier = Modifier.fillMaxWidth(), + selectedUsers = state.selectedUsers, + autoScroll = true, + onUserRemove = ::toggleUser, + contentPadding = PaddingValues(all = 16.dp), + ) + } + if (state.suggestions.isNotEmpty()) { + LazyColumn { + item { + ListSectionHeader( + title = stringResource(id = CommonStrings.common_suggestions), + hasDivider = false, + ) + } + itemsIndexed(state.suggestions) { index, invitableUser -> + CheckableUserRow( + checked = invitableUser.isSelected, + onCheckedChange = { + state.eventSink(DefaultInvitePeopleEvents.ToggleUser(invitableUser.matrixUser)) + }, + data = CheckableUserRowData.Resolved( + avatarData = invitableUser.matrixUser.getAvatarData(AvatarSize.UserListItem), + name = invitableUser.matrixUser.getBestName(), + subtext = invitableUser.matrixUser.userId.value, + ), + ) + if (index < state.suggestions.lastIndex) { + HorizontalDivider() + } + } + } + } } } } @@ -140,7 +174,7 @@ private fun InvitePeopleSearchBar( selectedUsers = selectedUsers, autoScroll = true, onUserRemove = onToggleUser, - contentPadding = PaddingValues(16.dp), + contentPadding = PaddingValues(all = 16.dp), ) } }, 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 388baf15e6..155282d0be 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 @@ -17,6 +17,7 @@ import io.element.android.libraries.designsystem.theme.components.SearchBarResul 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.JoinedRoom import io.element.android.libraries.matrix.api.room.RoomMembersState import io.element.android.libraries.matrix.api.room.RoomMembershipState @@ -26,7 +27,9 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.matrix.test.room.aRoomMemberList import io.element.android.libraries.matrix.ui.components.aMatrixUser @@ -67,13 +70,15 @@ internal class DefaultInvitePeoplePresenterTest { assertThat(initialState.canInvite).isFalse() assertThat(initialState.searchQuery).isEmpty() - skipItems(1) + cancelAndIgnoreRemainingEvents() } } @Test fun `present - updates search active state`() = runTest { - val presenter = createDefaultInvitePeoplePresenter() + val presenter = createDefaultInvitePeoplePresenter( + coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) + ) presenter.test { val initialState = awaitItem() skipItems(1) @@ -85,11 +90,12 @@ internal class DefaultInvitePeoplePresenterTest { resultState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query")) assertThat(awaitItemAsDefault().searchQuery).isEqualTo("some query") resultState.eventSink(InvitePeopleEvents.CloseSearch) - skipItems(1) + skipItems(2) awaitItemAsDefault().also { assertThat(it.isSearchActive).isFalse() assertThat(it.searchQuery).isEmpty() } + cancelAndIgnoreRemainingEvents() } } @@ -275,7 +281,7 @@ internal class DefaultInvitePeoplePresenterTest { val repository = FakeUserRepository() val presenter = createDefaultInvitePeoplePresenter( userRepository = repository, - coroutineDispatchers = testCoroutineDispatchers() + coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) ) presenter.test { val initialState = awaitItem() @@ -519,6 +525,85 @@ internal class DefaultInvitePeoplePresenterTest { } } + @Test + fun `present - suggestions are loaded from recent direct rooms`() = runTest { + val dmRoomId = RoomId("!dm_room:server.org") + val otherUserId = UserId("@frank:server.org") + val matrixClient = FakeMatrixClient(sessionId = A_USER_ID).apply { + // Track the DM room as recently visited + trackRecentlyVisitedRoom(dmRoomId) + // Set up a DM room with the other user + givenGetRoomResult( + dmRoomId, + FakeBaseRoom( + sessionId = A_USER_ID, + roomId = dmRoomId, + initialRoomInfo = aRoomInfo( + id = dmRoomId, + isDirect = true, + activeMembersCount = 2, + currentUserMembership = CurrentUserMembership.JOINED, + ), + getDirectRoomMemberResult = { aRoomMember(userId = otherUserId, displayName = "Frank") } + ) + ) + } + val presenter = createDefaultInvitePeoplePresenter( + matrixClient = matrixClient, + // Use empty room members so the suggestion doesn't get filtered + roomMembersState = RoomMembersState.Ready(persistentListOf()), + coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), + ) + presenter.test { + skipItems(2) + val state = awaitItemAsDefault() + assertThat(state.suggestions).hasSize(1) + assertThat(state.suggestions.first().matrixUser.userId).isEqualTo(otherUserId) + assertThat(state.suggestions.first().isSelected).isFalse() + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - suggestions filters out existing room members`() = runTest { + val dmRoomId = RoomId("!dm_room:server.org") + val alreadyJoinedUserId = UserId("@frank:server.org") + val matrixClient = FakeMatrixClient(sessionId = A_USER_ID).apply { + trackRecentlyVisitedRoom(dmRoomId) + givenGetRoomResult( + dmRoomId, + FakeBaseRoom( + sessionId = A_USER_ID, + roomId = dmRoomId, + initialRoomInfo = aRoomInfo( + id = dmRoomId, + isDirect = true, + activeMembersCount = 2, + currentUserMembership = CurrentUserMembership.JOINED, + ), + getDirectRoomMemberResult = { aRoomMember(userId = alreadyJoinedUserId, displayName = "Frank") } + ) + ) + } + // The user in the suggestion is already a member of the target room + val presenter = createDefaultInvitePeoplePresenter( + matrixClient = matrixClient, + roomMembersState = RoomMembersState.Ready( + persistentListOf( + aRoomMember(userId = alreadyJoinedUserId, membership = RoomMembershipState.JOIN) + ) + ), + coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), + ) + presenter.test { + skipItems(1) + // The suggestion should be filtered out because the user is already a room member + val state = awaitItemAsDefault() + assertThat(state.suggestions).isEmpty() + cancelAndIgnoreRemainingEvents() + } + } + private suspend fun FakeUserRepository.emitStateWithUsers( users: List, isSearching: Boolean = false diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/DefaultUserListPresenter.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/DefaultUserListPresenter.kt index 4e85dd0353..4bef68c757 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/DefaultUserListPresenter.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/DefaultUserListPresenter.kt @@ -31,6 +31,10 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.flow.toList + +private const val MAX_SUGGESTIONS_COUNT = 5 @AssistedInject class DefaultUserListPresenter( @@ -53,7 +57,10 @@ class DefaultUserListPresenter( override fun present(): UserListState { var recentDirectRooms by remember { mutableStateOf(emptyList()) } LaunchedEffect(Unit) { - recentDirectRooms = matrixClient.getRecentDirectRooms() + recentDirectRooms = matrixClient + .getRecentDirectRooms() + .take(MAX_SUGGESTIONS_COUNT) + .toList() } var isSearchActive by rememberSaveable { mutableStateOf(false) } val selectedUsers by userListDataStore.selectedUsers.collectAsState(emptyList()) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt index 481171ace0..589f88e9fd 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt @@ -85,6 +85,13 @@ interface BaseRoom : Closeable { */ suspend fun getUpdatedMember(userId: UserId): Result + /** + * Gets the direct room member, if any. + * This is a convenience method for getting the other member in a direct message room. + * Returns null if the room is not a dm or if the member cannot be found. + */ + suspend fun getDirectRoomMember(): RoomMember? + /** * Adds the room to the sync subscription list. */ diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/FilterRoomMembers.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/FilterRoomMembers.kt index 80cefc41e1..a164c9199d 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/FilterRoomMembers.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/FilterRoomMembers.kt @@ -17,10 +17,7 @@ import kotlin.coroutines.CoroutineContext * It does filter through the already known members, it doesn't perform additional requests. */ suspend fun BaseRoom.filterMembers(query: String, coroutineContext: CoroutineContext): List = withContext(coroutineContext) { - val roomMembersState = membersStateFlow.value - val activeRoomMembers = roomMembersState.roomMembers() - ?.filter { it.membership.isActive() } - .orEmpty() + val activeRoomMembers = membersStateFlow.value.activeRoomMembers() val filteredMembers = if (query.isBlank()) { activeRoomMembers } else { 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 index 8221674439..6db326c854 100644 --- 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 @@ -11,48 +11,35 @@ 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.BaseRoom import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.isDm import io.element.android.libraries.matrix.api.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 +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow 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() +/** + * Returns a [Flow] of [RecentDirectRoom] from recently visited DM rooms. + * The flow emits items lazily, allowing callers to filter and take only what they need. + * Use [kotlinx.coroutines.flow.take] to limit results and stop iteration early. + */ +fun MatrixClient.getRecentDirectRooms(): Flow = flow { 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 - } + val recentlyVisitedRooms = getRecentlyVisitedRooms().getOrDefault(emptyList()) + for (roomId in recentlyVisitedRooms) { + getRoom(roomId)?.use { room -> + val info = room.info() + if (info.isDm && info.currentUserMembership == CurrentUserMembership.JOINED) { + val otherUser = room.getDirectRoomMember()?.toMatrixUser() + if (otherUser != null && foundUserIds.add(otherUser.userId)) { + emit(RecentDirectRoom(room.roomId, otherUser)) } } + } } - return result -} - -suspend fun BaseRoom.isJoined(): Boolean { - return roomInfoFlow.first().currentUserMembership == CurrentUserMembership.JOINED } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt index 8c4847a73f..e73bed084e 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt @@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMembersState import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.draft.ComposerDraft +import io.element.android.libraries.matrix.api.room.isDm import io.element.android.libraries.matrix.api.room.powerlevels.RoomPermissions import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues import io.element.android.libraries.matrix.api.room.tombstone.PredecessorRoom @@ -112,6 +113,20 @@ class RustBaseRoom( } } + override suspend fun getDirectRoomMember(): RoomMember? = withContext(roomDispatcher) { + runCatchingExceptions { + if (info().isDm) { + innerRoom.membersNoSync().use { members -> + members.nextChunk(members.len()) + ?.map(RoomMemberMapper::map) + ?.firstOrNull { roomMember -> roomMember.userId != sessionId && roomMember.membership.isActive() } + } + } else { + null + } + }.getOrNull() + } + override suspend fun getUpdatedMember(userId: UserId): Result = withContext(roomDispatcher) { runCatchingExceptions { RoomMemberMapper.map(innerRoom.member(userId.value)) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt index 78765d1ec4..4ceddc414c 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt @@ -55,6 +55,7 @@ class FakeBaseRoom( private val leaveRoomLambda: () -> Result = { lambdaError() }, private var updateMembersResult: () -> Unit = { lambdaError() }, private val getMembersResult: (Int) -> Result> = { lambdaError() }, + private val getDirectRoomMemberResult: () -> RoomMember? = { null }, private val saveComposerDraftLambda: (ComposerDraft) -> Result = { _: ComposerDraft -> Result.success(Unit) }, private val loadComposerDraftLambda: () -> Result = { Result.success(null) }, private val clearComposerDraftLambda: () -> Result = { Result.success(Unit) }, @@ -90,6 +91,10 @@ class FakeBaseRoom( return getMembersResult(limit) } + override suspend fun getDirectRoomMember(): RoomMember? { + return getDirectRoomMemberResult() + } + override suspend fun subscribeToSync() { subscribeToSyncLambda() }