Add suggestions section to InvitePeopleView

This commit is contained in:
ganfra 2026-01-19 17:35:39 +01:00
parent 23d3066a38
commit 7be66061e2
11 changed files with 227 additions and 48 deletions

View file

@ -11,6 +11,7 @@ package io.element.android.features.invitepeople.impl
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState 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.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState 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.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.matrix.api.user.MatrixUser
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.libraries.usersearch.api.UserRepository 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.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
private const val MAX_SUGGESTIONS_COUNT = 5
@AssistedInject @AssistedInject
class DefaultInvitePeoplePresenter( class DefaultInvitePeoplePresenter(
@Assisted private val joinedRoom: JoinedRoom?, @Assisted private val joinedRoom: JoinedRoom?,
@ -78,6 +85,34 @@ class DefaultInvitePeoplePresenter(
val showSearchLoader = rememberSaveable { mutableStateOf(false) } val showSearchLoader = rememberSaveable { mutableStateOf(false) }
val sendInvitesAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) } val sendInvitesAction = remember { mutableStateOf<AsyncAction<Unit>>(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()) { val room by produceState(if (joinedRoom != null) AsyncData.Success(joinedRoom) else AsyncData.Loading()) {
if (joinedRoom == null) { if (joinedRoom == null) {
val result = matrixClient.getJoinedRoom(roomId) val result = matrixClient.getJoinedRoom(roomId)
@ -118,6 +153,7 @@ class DefaultInvitePeoplePresenter(
is DefaultInvitePeopleEvents.ToggleUser -> { is DefaultInvitePeopleEvents.ToggleUser -> {
selectedUsers.toggleUser(event.user) selectedUsers.toggleUser(event.user)
searchResults.toggleUser(event.user) searchResults.toggleUser(event.user)
// suggestions will automatically update via derivedStateOf when selectedUsers changes
} }
is InvitePeopleEvents.SendInvites -> { is InvitePeopleEvents.SendInvites -> {
room.dataOrNull()?.let { room.dataOrNull()?.let {
@ -140,6 +176,7 @@ class DefaultInvitePeoplePresenter(
searchResults = searchResults.value, searchResults = searchResults.value,
showSearchLoader = showSearchLoader.value, showSearchLoader = showSearchLoader.value,
sendInvitesAction = sendInvitesAction.value, sendInvitesAction = sendInvitesAction.value,
suggestions = suggestions,
eventSink = ::handleEvent, eventSink = ::handleEvent,
) )
} }

View file

@ -25,5 +25,6 @@ data class DefaultInvitePeopleState(
val selectedUsers: ImmutableList<MatrixUser>, val selectedUsers: ImmutableList<MatrixUser>,
override val isSearchActive: Boolean, override val isSearchActive: Boolean,
override val sendInvitesAction: AsyncAction<Unit>, override val sendInvitesAction: AsyncAction<Unit>,
val suggestions: ImmutableList<InvitableUser>,
override val eventSink: (InvitePeopleEvents) -> Unit override val eventSink: (InvitePeopleEvents) -> Unit
) : InvitePeopleState ) : InvitePeopleState

View file

@ -101,6 +101,9 @@ private fun aDefaultInvitePeopleState(
isSearchActive: Boolean = false, isSearchActive: Boolean = false,
showSearchLoader: Boolean = false, showSearchLoader: Boolean = false,
sendInvitesAction: AsyncAction<Unit> = AsyncAction.Uninitialized, sendInvitesAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
suggestions: List<InvitableUser> = aMatrixUserList()
.take(5)
.map { user -> anInvitableUser(matrixUser = user, isSelected = user in selectedUsers) },
): DefaultInvitePeopleState { ): DefaultInvitePeopleState {
return DefaultInvitePeopleState( return DefaultInvitePeopleState(
room = room, room = room,
@ -111,6 +114,7 @@ private fun aDefaultInvitePeopleState(
isSearchActive = isSearchActive, isSearchActive = isSearchActive,
showSearchLoader = showSearchLoader, showSearchLoader = showSearchLoader,
sendInvitesAction = sendInvitesAction, sendInvitesAction = sendInvitesAction,
suggestions = suggestions.toImmutableList(),
eventSink = {}, eventSink = {},
) )
} }

View file

@ -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.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight 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.SearchBar
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
@ -82,6 +84,10 @@ private fun InvitePeopleContentView(
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp),
) { ) {
fun toggleUser(user: MatrixUser) {
state.eventSink(DefaultInvitePeopleEvents.ToggleUser(user))
}
InvitePeopleSearchBar( InvitePeopleSearchBar(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
query = state.searchQuery, query = state.searchQuery,
@ -97,17 +103,45 @@ private fun InvitePeopleContentView(
) )
}, },
onTextChange = { state.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery(it)) }, onTextChange = { state.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery(it)) },
onToggleUser = { state.eventSink(DefaultInvitePeopleEvents.ToggleUser(it)) }, onToggleUser = ::toggleUser,
) )
if (!state.isSearchActive) { if (!state.isSearchActive) {
SelectedUsersRowList( if (state.selectedUsers.isNotEmpty()) {
modifier = Modifier.fillMaxWidth(), SelectedUsersRowList(
selectedUsers = state.selectedUsers, modifier = Modifier.fillMaxWidth(),
autoScroll = true, selectedUsers = state.selectedUsers,
onUserRemove = { state.eventSink(DefaultInvitePeopleEvents.ToggleUser(it)) }, autoScroll = true,
contentPadding = PaddingValues(16.dp), 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, selectedUsers = selectedUsers,
autoScroll = true, autoScroll = true,
onUserRemove = onToggleUser, onUserRemove = onToggleUser,
contentPadding = PaddingValues(16.dp), contentPadding = PaddingValues(all = 16.dp),
) )
} }
}, },

View file

@ -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.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId 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.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.JoinedRoom
import io.element.android.libraries.matrix.api.room.RoomMembersState import io.element.android.libraries.matrix.api.room.RoomMembersState
import io.element.android.libraries.matrix.api.room.RoomMembershipState 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
import io.element.android.libraries.matrix.test.A_USER_ID_2 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.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.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.aRoomMember
import io.element.android.libraries.matrix.test.room.aRoomMemberList import io.element.android.libraries.matrix.test.room.aRoomMemberList
import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.matrix.ui.components.aMatrixUser
@ -67,13 +70,15 @@ internal class DefaultInvitePeoplePresenterTest {
assertThat(initialState.canInvite).isFalse() assertThat(initialState.canInvite).isFalse()
assertThat(initialState.searchQuery).isEmpty() assertThat(initialState.searchQuery).isEmpty()
skipItems(1) cancelAndIgnoreRemainingEvents()
} }
} }
@Test @Test
fun `present - updates search active state`() = runTest { fun `present - updates search active state`() = runTest {
val presenter = createDefaultInvitePeoplePresenter() val presenter = createDefaultInvitePeoplePresenter(
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
)
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
skipItems(1) skipItems(1)
@ -85,11 +90,12 @@ internal class DefaultInvitePeoplePresenterTest {
resultState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query")) resultState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query"))
assertThat(awaitItemAsDefault().searchQuery).isEqualTo("some query") assertThat(awaitItemAsDefault().searchQuery).isEqualTo("some query")
resultState.eventSink(InvitePeopleEvents.CloseSearch) resultState.eventSink(InvitePeopleEvents.CloseSearch)
skipItems(1) skipItems(2)
awaitItemAsDefault().also { awaitItemAsDefault().also {
assertThat(it.isSearchActive).isFalse() assertThat(it.isSearchActive).isFalse()
assertThat(it.searchQuery).isEmpty() assertThat(it.searchQuery).isEmpty()
} }
cancelAndIgnoreRemainingEvents()
} }
} }
@ -275,7 +281,7 @@ internal class DefaultInvitePeoplePresenterTest {
val repository = FakeUserRepository() val repository = FakeUserRepository()
val presenter = createDefaultInvitePeoplePresenter( val presenter = createDefaultInvitePeoplePresenter(
userRepository = repository, userRepository = repository,
coroutineDispatchers = testCoroutineDispatchers() coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
) )
presenter.test { presenter.test {
val initialState = awaitItem() 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( private suspend fun FakeUserRepository.emitStateWithUsers(
users: List<MatrixUser>, users: List<MatrixUser>,
isSearching: Boolean = false isSearching: Boolean = false

View file

@ -31,6 +31,10 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.toList
private const val MAX_SUGGESTIONS_COUNT = 5
@AssistedInject @AssistedInject
class DefaultUserListPresenter( class DefaultUserListPresenter(
@ -53,7 +57,10 @@ class DefaultUserListPresenter(
override fun present(): UserListState { override fun present(): UserListState {
var recentDirectRooms by remember { mutableStateOf(emptyList<RecentDirectRoom>()) } var recentDirectRooms by remember { mutableStateOf(emptyList<RecentDirectRoom>()) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
recentDirectRooms = matrixClient.getRecentDirectRooms() recentDirectRooms = matrixClient
.getRecentDirectRooms()
.take(MAX_SUGGESTIONS_COUNT)
.toList()
} }
var isSearchActive by rememberSaveable { mutableStateOf(false) } var isSearchActive by rememberSaveable { mutableStateOf(false) }
val selectedUsers by userListDataStore.selectedUsers.collectAsState(emptyList()) val selectedUsers by userListDataStore.selectedUsers.collectAsState(emptyList())

View file

@ -85,6 +85,13 @@ interface BaseRoom : Closeable {
*/ */
suspend fun getUpdatedMember(userId: UserId): Result<RoomMember> suspend fun getUpdatedMember(userId: UserId): Result<RoomMember>
/**
* 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. * Adds the room to the sync subscription list.
*/ */

View file

@ -17,10 +17,7 @@ import kotlin.coroutines.CoroutineContext
* It does filter through the already known members, it doesn't perform additional requests. * It does filter through the already known members, it doesn't perform additional requests.
*/ */
suspend fun BaseRoom.filterMembers(query: String, coroutineContext: CoroutineContext): List<RoomMember> = withContext(coroutineContext) { suspend fun BaseRoom.filterMembers(query: String, coroutineContext: CoroutineContext): List<RoomMember> = withContext(coroutineContext) {
val roomMembersState = membersStateFlow.value val activeRoomMembers = membersStateFlow.value.activeRoomMembers()
val activeRoomMembers = roomMembersState.roomMembers()
?.filter { it.membership.isActive() }
.orEmpty()
val filteredMembers = if (query.isBlank()) { val filteredMembers = if (query.isBlank()) {
activeRoomMembers activeRoomMembers
} else { } else {

View file

@ -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.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId 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.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.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.isDm 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.room.toMatrixUser
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
private const val MAX_RECENT_DIRECT_ROOMS_TO_RETURN = 5
data class RecentDirectRoom( data class RecentDirectRoom(
val roomId: RoomId, val roomId: RoomId,
val matrixUser: MatrixUser, val matrixUser: MatrixUser,
) )
suspend fun MatrixClient.getRecentDirectRooms( /**
maxNumberOfResults: Int = MAX_RECENT_DIRECT_ROOMS_TO_RETURN, * Returns a [Flow] of [RecentDirectRoom] from recently visited DM rooms.
): List<RecentDirectRoom> { * The flow emits items lazily, allowing callers to filter and take only what they need.
val result = mutableListOf<RecentDirectRoom>() * Use [kotlinx.coroutines.flow.take] to limit results and stop iteration early.
*/
fun MatrixClient.getRecentDirectRooms(): Flow<RecentDirectRoom> = flow {
val foundUserIds = mutableSetOf<UserId>() val foundUserIds = mutableSetOf<UserId>()
getRecentlyVisitedRooms().getOrNull()?.let { roomIds -> val recentlyVisitedRooms = getRecentlyVisitedRooms().getOrDefault(emptyList())
roomIds for (roomId in recentlyVisitedRooms) {
.mapNotNull { roomId -> getRoom(roomId) } getRoom(roomId)?.use { room ->
.filter { it.isDm() && it.isJoined() } val info = room.info()
.map { room -> if (info.isDm && info.currentUserMembership == CurrentUserMembership.JOINED) {
val otherUser = room.getMembers().getOrNull() val otherUser = room.getDirectRoomMember()?.toMatrixUser()
?.firstOrNull { it.userId != sessionId } if (otherUser != null && foundUserIds.add(otherUser.userId)) {
?.takeIf { foundUserIds.add(it.userId) } emit(RecentDirectRoom(room.roomId, otherUser))
?.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 BaseRoom.isJoined(): Boolean {
return roomInfoFlow.first().currentUserMembership == CurrentUserMembership.JOINED
} }

View file

@ -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.RoomMembersState
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver 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.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.RoomPermissions
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues
import io.element.android.libraries.matrix.api.room.tombstone.PredecessorRoom 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<RoomMember> = withContext(roomDispatcher) { override suspend fun getUpdatedMember(userId: UserId): Result<RoomMember> = withContext(roomDispatcher) {
runCatchingExceptions { runCatchingExceptions {
RoomMemberMapper.map(innerRoom.member(userId.value)) RoomMemberMapper.map(innerRoom.member(userId.value))

View file

@ -55,6 +55,7 @@ class FakeBaseRoom(
private val leaveRoomLambda: () -> Result<Unit> = { lambdaError() }, private val leaveRoomLambda: () -> Result<Unit> = { lambdaError() },
private var updateMembersResult: () -> Unit = { lambdaError() }, private var updateMembersResult: () -> Unit = { lambdaError() },
private val getMembersResult: (Int) -> Result<List<RoomMember>> = { lambdaError() }, private val getMembersResult: (Int) -> Result<List<RoomMember>> = { lambdaError() },
private val getDirectRoomMemberResult: () -> RoomMember? = { null },
private val saveComposerDraftLambda: (ComposerDraft) -> Result<Unit> = { _: ComposerDraft -> Result.success(Unit) }, private val saveComposerDraftLambda: (ComposerDraft) -> Result<Unit> = { _: ComposerDraft -> Result.success(Unit) },
private val loadComposerDraftLambda: () -> Result<ComposerDraft?> = { Result.success<ComposerDraft?>(null) }, private val loadComposerDraftLambda: () -> Result<ComposerDraft?> = { Result.success<ComposerDraft?>(null) },
private val clearComposerDraftLambda: () -> Result<Unit> = { Result.success(Unit) }, private val clearComposerDraftLambda: () -> Result<Unit> = { Result.success(Unit) },
@ -90,6 +91,10 @@ class FakeBaseRoom(
return getMembersResult(limit) return getMembersResult(limit)
} }
override suspend fun getDirectRoomMember(): RoomMember? {
return getDirectRoomMemberResult()
}
override suspend fun subscribeToSync() { override suspend fun subscribeToSync() {
subscribeToSyncLambda() subscribeToSyncLambda()
} }