Merge pull request #6045 from element-hq/feature/fga/invite_people_suggestions

Add suggestions section to InvitePeopleView
This commit is contained in:
ganfra 2026-01-20 13:45:42 +01:00 committed by GitHub
commit 1cbf7d9624
17 changed files with 239 additions and 60 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()
} }

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:070f37faf63ee4bb7acabfbe19943181b367cb53d23978efe9ca669d6051e04c oid sha256:267f997e9f23c0745f8ac27216c125da7eb78db9f7fa42e670e5e2b40b8eeec2
size 9137 size 37777

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e1a765e16b340e75d0657de9bbbb429d09ebffae9e94deee982a8bcfc74e21bf oid sha256:5ffe121ccdc83d57224397ffd5ad5fc98acec3fd4b5eb5f495c173365ff39edd
size 21454 size 50791

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e1a765e16b340e75d0657de9bbbb429d09ebffae9e94deee982a8bcfc74e21bf oid sha256:5ffe121ccdc83d57224397ffd5ad5fc98acec3fd4b5eb5f495c173365ff39edd
size 21454 size 50791

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:1787bd92c75748e69f346f7588170bf6fa9c626dd6544f4270f30f0f9b614037 oid sha256:b7fdff51735509f84dcf9c51ffcf813bfd0844fd7ba4374b39a969e1f00089c0
size 8631 size 37941

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7c4c5d0035baf55bc67abf83eae90fa0c16e044a0d0ab26634c02fb0ad5d4c26 oid sha256:dada059f12c0fe97f63fed9e63e04e174f8b285a0f40b33e1d49e86039b9201c
size 22062 size 51905

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7c4c5d0035baf55bc67abf83eae90fa0c16e044a0d0ab26634c02fb0ad5d4c26 oid sha256:dada059f12c0fe97f63fed9e63e04e174f8b285a0f40b33e1d49e86039b9201c
size 22062 size 51905