Improve room member list loading UX (#2543)
Improve room member list UX: - Don't display the list in chunks anymore. - Use an indeterminate linear progress indicator to display some loading is being done (either loading the cached list or the updated one). - Try to make sure we don't display the members loaded from timeline items as the cached room list by mistake. * Update screenshots * Simplify member loading logic. --------- Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
parent
f0fcbb8ecd
commit
1670909408
19 changed files with 268 additions and 196 deletions
1
changelog.d/2452.misc
Normal file
1
changelog.d/2452.misc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Improve room member list loading UX.
|
||||||
|
|
@ -31,7 +31,6 @@ import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationEvents
|
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationEvents
|
||||||
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationPresenter
|
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationPresenter
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
|
|
@ -43,6 +42,7 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||||
import io.element.android.libraries.matrix.api.room.powerlevels.canInvite
|
import io.element.android.libraries.matrix.api.room.powerlevels.canInvite
|
||||||
import io.element.android.libraries.matrix.api.room.roomMembers
|
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
|
@ -62,7 +62,7 @@ class RoomMemberListPresenter @AssistedInject constructor(
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): RoomMemberListState {
|
override fun present(): RoomMemberListState {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
var roomMembers by remember { mutableStateOf<AsyncData<RoomMembers>>(AsyncData.Loading()) }
|
var roomMembers by remember { mutableStateOf(RoomMembers.loading()) }
|
||||||
var searchQuery by rememberSaveable { mutableStateOf("") }
|
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||||
var searchResults by remember {
|
var searchResults by remember {
|
||||||
mutableStateOf<SearchBarResultState<RoomMembers>>(SearchBarResultState.Initial())
|
mutableStateOf<SearchBarResultState<RoomMembers>>(SearchBarResultState.Initial())
|
||||||
|
|
@ -90,21 +90,26 @@ class RoomMemberListPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
withContext(coroutineDispatchers.io) {
|
withContext(coroutineDispatchers.io) {
|
||||||
val members = membersState.roomMembers().orEmpty().groupBy { it.membership }
|
val members = membersState.roomMembers().orEmpty().groupBy { it.membership }
|
||||||
roomMembers = AsyncData.Success(
|
val info = room.roomInfoFlow.first()
|
||||||
RoomMembers(
|
if (members.getOrDefault(RoomMembershipState.JOIN, emptyList()).size < info.joinedMembersCount / 2) {
|
||||||
invited = members.getOrDefault(RoomMembershipState.INVITE, emptyList()).toImmutableList(),
|
// Don't display initial room member list if we have less than half of the joined members:
|
||||||
joined = members.getOrDefault(RoomMembershipState.JOIN, emptyList())
|
// This result will come from the timeline loading membership events and it'll be wrong.
|
||||||
.sortedWith(PowerLevelRoomMemberComparator())
|
return@withContext
|
||||||
.toImmutableList(),
|
}
|
||||||
banned = members.getOrDefault(RoomMembershipState.BAN, emptyList()).sortedBy { it.userId.value }.toImmutableList(),
|
roomMembers = RoomMembers(
|
||||||
)
|
invited = members.getOrDefault(RoomMembershipState.INVITE, emptyList()).toImmutableList(),
|
||||||
|
joined = members.getOrDefault(RoomMembershipState.JOIN, emptyList())
|
||||||
|
.sortedWith(PowerLevelRoomMemberComparator())
|
||||||
|
.toImmutableList(),
|
||||||
|
banned = members.getOrDefault(RoomMembershipState.BAN, emptyList()).sortedBy { it.userId.value }.toImmutableList(),
|
||||||
|
isLoading = membersState is MatrixRoomMembersState.Pending,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(searchQuery) {
|
LaunchedEffect(membersState, searchQuery, isSearchActive) {
|
||||||
withContext(coroutineDispatchers.io) {
|
withContext(coroutineDispatchers.io) {
|
||||||
searchResults = if (searchQuery.isEmpty()) {
|
searchResults = if (searchQuery.isEmpty() || !isSearchActive) {
|
||||||
SearchBarResultState.Initial()
|
SearchBarResultState.Initial()
|
||||||
} else {
|
} else {
|
||||||
val results = roomMemberListDataSource.search(searchQuery).groupBy { it.membership }
|
val results = roomMemberListDataSource.search(searchQuery).groupBy { it.membership }
|
||||||
|
|
@ -118,6 +123,7 @@ class RoomMemberListPresenter @AssistedInject constructor(
|
||||||
.sortedWith(PowerLevelRoomMemberComparator())
|
.sortedWith(PowerLevelRoomMemberComparator())
|
||||||
.toImmutableList(),
|
.toImmutableList(),
|
||||||
banned = results.getOrDefault(RoomMembershipState.BAN, emptyList()).sortedBy { it.userId.value }.toImmutableList(),
|
banned = results.getOrDefault(RoomMembershipState.BAN, emptyList()).sortedBy { it.userId.value }.toImmutableList(),
|
||||||
|
isLoading = membersState is MatrixRoomMembersState.Pending,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,13 @@
|
||||||
package io.element.android.features.roomdetails.impl.members
|
package io.element.android.features.roomdetails.impl.members
|
||||||
|
|
||||||
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationState
|
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationState
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
data class RoomMemberListState(
|
data class RoomMemberListState(
|
||||||
val roomMembers: AsyncData<RoomMembers>,
|
val roomMembers: RoomMembers,
|
||||||
val searchQuery: String,
|
val searchQuery: String,
|
||||||
val searchResults: SearchBarResultState<RoomMembers>,
|
val searchResults: SearchBarResultState<RoomMembers>,
|
||||||
val isSearchActive: Boolean,
|
val isSearchActive: Boolean,
|
||||||
|
|
@ -36,4 +36,14 @@ data class RoomMembers(
|
||||||
val invited: ImmutableList<RoomMember>,
|
val invited: ImmutableList<RoomMember>,
|
||||||
val joined: ImmutableList<RoomMember>,
|
val joined: ImmutableList<RoomMember>,
|
||||||
val banned: ImmutableList<RoomMember>,
|
val banned: ImmutableList<RoomMember>,
|
||||||
)
|
val isLoading: Boolean,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun loading() = RoomMembers(
|
||||||
|
invited = persistentListOf(),
|
||||||
|
joined = persistentListOf(),
|
||||||
|
banned = persistentListOf(),
|
||||||
|
isLoading = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ package io.element.android.features.roomdetails.impl.members
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationState
|
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationState
|
||||||
import io.element.android.features.roomdetails.impl.members.moderation.aRoomMembersModerationState
|
import io.element.android.features.roomdetails.impl.members.moderation.aRoomMembersModerationState
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
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.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
|
|
@ -30,15 +29,14 @@ internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMember
|
||||||
override val values: Sequence<RoomMemberListState>
|
override val values: Sequence<RoomMemberListState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aRoomMemberListState(
|
aRoomMemberListState(
|
||||||
roomMembers = AsyncData.Success(
|
roomMembers = RoomMembers(
|
||||||
RoomMembers(
|
invited = persistentListOf(aVictor(), aWalter()),
|
||||||
invited = persistentListOf(aVictor(), aWalter()),
|
joined = persistentListOf(anAlice(), aBob(), aWalter()),
|
||||||
joined = persistentListOf(anAlice(), aBob(), aWalter()),
|
banned = persistentListOf(),
|
||||||
banned = persistentListOf(),
|
isLoading = false,
|
||||||
)
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
aRoomMemberListState(roomMembers = AsyncData.Loading()),
|
aRoomMemberListState(roomMembers = RoomMembers.loading()),
|
||||||
aRoomMemberListState().copy(canInvite = true),
|
aRoomMemberListState().copy(canInvite = true),
|
||||||
aRoomMemberListState().copy(isSearchActive = false),
|
aRoomMemberListState().copy(isSearchActive = false),
|
||||||
aRoomMemberListState().copy(isSearchActive = true),
|
aRoomMemberListState().copy(isSearchActive = true),
|
||||||
|
|
@ -51,6 +49,7 @@ internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMember
|
||||||
invited = persistentListOf(aVictor()),
|
invited = persistentListOf(aVictor()),
|
||||||
joined = persistentListOf(anAlice()),
|
joined = persistentListOf(anAlice()),
|
||||||
banned = persistentListOf(),
|
banned = persistentListOf(),
|
||||||
|
isLoading = false,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -66,26 +65,37 @@ internal class RoomMemberListStateBannedProvider : PreviewParameterProvider<Room
|
||||||
override val values: Sequence<RoomMemberListState>
|
override val values: Sequence<RoomMemberListState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aRoomMemberListState(
|
aRoomMemberListState(
|
||||||
roomMembers = AsyncData.Success(
|
roomMembers = RoomMembers(
|
||||||
RoomMembers(
|
invited = persistentListOf(),
|
||||||
invited = persistentListOf(),
|
joined = persistentListOf(),
|
||||||
joined = persistentListOf(),
|
banned = persistentListOf(
|
||||||
banned = persistentListOf(
|
aRoomMember(userId = UserId("@alice:example.com"), displayName = "Alice"),
|
||||||
aRoomMember(userId = UserId("@alice:example.com"), displayName = "Alice"),
|
aRoomMember(userId = UserId("@bob:example.com"), displayName = "Bob"),
|
||||||
aRoomMember(userId = UserId("@bob:example.com"), displayName = "Bob"),
|
aRoomMember(userId = UserId("@charlie:example.com"), displayName = "Charlie"),
|
||||||
aRoomMember(userId = UserId("@charlie:example.com"), displayName = "Charlie"),
|
),
|
||||||
),
|
isLoading = false,
|
||||||
)
|
|
||||||
),
|
),
|
||||||
moderationState = aRoomMembersModerationState(canDisplayBannedUsers = true),
|
moderationState = aRoomMembersModerationState(canDisplayBannedUsers = true),
|
||||||
),
|
),
|
||||||
aRoomMemberListState(
|
aRoomMemberListState(
|
||||||
roomMembers = AsyncData.Success(
|
roomMembers = RoomMembers(
|
||||||
RoomMembers(
|
invited = persistentListOf(),
|
||||||
invited = persistentListOf(),
|
joined = persistentListOf(),
|
||||||
joined = persistentListOf(),
|
banned = persistentListOf(
|
||||||
banned = persistentListOf(),
|
aRoomMember(userId = UserId("@alice:example.com"), displayName = "Alice"),
|
||||||
)
|
aRoomMember(userId = UserId("@bob:example.com"), displayName = "Bob"),
|
||||||
|
aRoomMember(userId = UserId("@charlie:example.com"), displayName = "Charlie"),
|
||||||
|
),
|
||||||
|
isLoading = true,
|
||||||
|
),
|
||||||
|
moderationState = aRoomMembersModerationState(canDisplayBannedUsers = true),
|
||||||
|
),
|
||||||
|
aRoomMemberListState(
|
||||||
|
roomMembers = RoomMembers(
|
||||||
|
invited = persistentListOf(),
|
||||||
|
joined = persistentListOf(),
|
||||||
|
banned = persistentListOf(),
|
||||||
|
isLoading = false,
|
||||||
),
|
),
|
||||||
moderationState = aRoomMembersModerationState(canDisplayBannedUsers = true),
|
moderationState = aRoomMembersModerationState(canDisplayBannedUsers = true),
|
||||||
)
|
)
|
||||||
|
|
@ -93,7 +103,7 @@ internal class RoomMemberListStateBannedProvider : PreviewParameterProvider<Room
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun aRoomMemberListState(
|
internal fun aRoomMemberListState(
|
||||||
roomMembers: AsyncData<RoomMembers> = AsyncData.Uninitialized,
|
roomMembers: RoomMembers = RoomMembers.loading(),
|
||||||
searchResults: SearchBarResultState<RoomMembers> = SearchBarResultState.Initial(),
|
searchResults: SearchBarResultState<RoomMembers> = SearchBarResultState.Initial(),
|
||||||
moderationState: RoomMembersModerationState = aRoomMembersModerationState(),
|
moderationState: RoomMembersModerationState = aRoomMembersModerationState(),
|
||||||
) = RoomMemberListState(
|
) = RoomMemberListState(
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,11 @@
|
||||||
|
|
||||||
package io.element.android.features.roomdetails.impl.members
|
package io.element.android.features.roomdetails.impl.members
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.expandVertically
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
|
@ -23,7 +28,6 @@ import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
|
@ -49,13 +53,12 @@ import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.features.roomdetails.impl.R
|
import io.element.android.features.roomdetails.impl.R
|
||||||
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationView
|
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationView
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
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.aliasScreenTitle
|
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
||||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
import io.element.android.libraries.designsystem.theme.components.LinearProgressIndicator
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
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
|
||||||
|
|
@ -124,20 +127,15 @@ fun RoomMemberListView(
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!state.isSearchActive) {
|
if (!state.isSearchActive) {
|
||||||
if (state.roomMembers is AsyncData.Success) {
|
RoomMemberList(
|
||||||
RoomMemberList(
|
isLoading = state.roomMembers.isLoading,
|
||||||
roomMembers = state.roomMembers.data,
|
roomMembers = state.roomMembers,
|
||||||
showMembersCount = true,
|
showMembersCount = true,
|
||||||
canDisplayBannedUsersControls = state.moderationState.canDisplayBannedUsers,
|
canDisplayBannedUsersControls = state.moderationState.canDisplayBannedUsers,
|
||||||
selectedSection = selectedSection,
|
selectedSection = selectedSection,
|
||||||
onSelectedSectionChanged = { selectedSection = it },
|
onSelectedSectionChanged = { selectedSection = it },
|
||||||
onUserSelected = ::onUserSelected,
|
onUserSelected = ::onUserSelected,
|
||||||
)
|
)
|
||||||
} else if (state.roomMembers.isLoading()) {
|
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
|
||||||
CircularProgressIndicator()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -151,6 +149,7 @@ fun RoomMemberListView(
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun RoomMemberList(
|
private fun RoomMemberList(
|
||||||
|
isLoading: Boolean,
|
||||||
roomMembers: RoomMembers,
|
roomMembers: RoomMembers,
|
||||||
showMembersCount: Boolean,
|
showMembersCount: Boolean,
|
||||||
selectedSection: SelectedSection,
|
selectedSection: SelectedSection,
|
||||||
|
|
@ -159,28 +158,37 @@ private fun RoomMemberList(
|
||||||
onUserSelected: (RoomMember) -> Unit,
|
onUserSelected: (RoomMember) -> Unit,
|
||||||
) {
|
) {
|
||||||
LazyColumn(modifier = Modifier.fillMaxWidth(), state = rememberLazyListState()) {
|
LazyColumn(modifier = Modifier.fillMaxWidth(), state = rememberLazyListState()) {
|
||||||
if (canDisplayBannedUsersControls) {
|
stickyHeader {
|
||||||
stickyHeader {
|
Column {
|
||||||
val segmentedButtonTitles = persistentListOf(
|
if (canDisplayBannedUsersControls) {
|
||||||
stringResource(id = R.string.screen_room_member_list_mode_members),
|
val segmentedButtonTitles = persistentListOf(
|
||||||
stringResource(id = R.string.screen_room_member_list_mode_banned),
|
stringResource(id = R.string.screen_room_member_list_mode_members),
|
||||||
)
|
stringResource(id = R.string.screen_room_member_list_mode_banned),
|
||||||
SingleChoiceSegmentedButtonRow(
|
)
|
||||||
modifier = Modifier
|
SingleChoiceSegmentedButtonRow(
|
||||||
.background(ElementTheme.colors.bgCanvasDefault)
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.background(ElementTheme.colors.bgCanvasDefault)
|
||||||
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
|
.fillMaxWidth()
|
||||||
) {
|
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
|
||||||
for ((index, title) in segmentedButtonTitles.withIndex()) {
|
) {
|
||||||
SegmentedButton(
|
for ((index, title) in segmentedButtonTitles.withIndex()) {
|
||||||
index = index,
|
SegmentedButton(
|
||||||
count = segmentedButtonTitles.size,
|
index = index,
|
||||||
selected = selectedSection.ordinal == index,
|
count = segmentedButtonTitles.size,
|
||||||
onClick = { onSelectedSectionChanged(SelectedSection.entries[index]) },
|
selected = selectedSection.ordinal == index,
|
||||||
text = title,
|
onClick = { onSelectedSectionChanged(SelectedSection.entries[index]) },
|
||||||
)
|
text = title,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isLoading,
|
||||||
|
enter = fadeIn() + expandVertically(),
|
||||||
|
exit = fadeOut() + shrinkVertically(),
|
||||||
|
) {
|
||||||
|
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
when (selectedSection) {
|
when (selectedSection) {
|
||||||
|
|
@ -335,6 +343,7 @@ private fun RoomMemberSearchBar(
|
||||||
resultState = state,
|
resultState = state,
|
||||||
resultHandler = { results ->
|
resultHandler = { results ->
|
||||||
RoomMemberList(
|
RoomMemberList(
|
||||||
|
isLoading = false,
|
||||||
roomMembers = results,
|
roomMembers = results,
|
||||||
showMembersCount = false,
|
showMembersCount = false,
|
||||||
onUserSelected = { onUserSelected(it) },
|
onUserSelected = { onUserSelected(it) },
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ import io.element.android.features.roomdetails.impl.members.aWalter
|
||||||
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationEvents
|
import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationEvents
|
||||||
import io.element.android.features.roomdetails.impl.members.moderation.aRoomMembersModerationState
|
import io.element.android.features.roomdetails.impl.members.moderation.aRoomMembersModerationState
|
||||||
import io.element.android.features.roomdetails.members.moderation.FakeRoomMembersModerationPresenter
|
import io.element.android.features.roomdetails.members.moderation.FakeRoomMembersModerationPresenter
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
|
|
@ -40,6 +39,7 @@ import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
|
@ -54,25 +54,28 @@ class RoomMemberListPresenterTests {
|
||||||
val warmUpRule = WarmUpRule()
|
val warmUpRule = WarmUpRule()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `search is done automatically on start, but is async`() = runTest {
|
fun `member loading is done automatically on start, but is async`() = runTest {
|
||||||
val room = FakeMatrixRoom()
|
val room = FakeMatrixRoom().apply {
|
||||||
|
// Needed to avoid discarding the loaded members as a partial and invalid result
|
||||||
|
givenRoomInfo(aRoomInfo(joinedMembersCount = 2))
|
||||||
|
}
|
||||||
val presenter = createPresenter(matrixRoom = room)
|
val presenter = createPresenter(matrixRoom = room)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.roomMembers).isInstanceOf(AsyncData.Loading::class.java)
|
assertThat(initialState.roomMembers.isLoading).isTrue()
|
||||||
assertThat(initialState.searchQuery).isEmpty()
|
assertThat(initialState.searchQuery).isEmpty()
|
||||||
assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java)
|
assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java)
|
||||||
assertThat(initialState.isSearchActive).isFalse()
|
assertThat(initialState.isSearchActive).isFalse()
|
||||||
room.givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList()))
|
room.givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList()))
|
||||||
// Skip item while the new members state is processed
|
// Skip item while the new members state is processed
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
val loadedState = awaitItem()
|
val loadedMembersState = awaitItem()
|
||||||
assertThat(loadedState.roomMembers).isInstanceOf(AsyncData.Success::class.java)
|
assertThat(loadedMembersState.roomMembers.isLoading).isFalse()
|
||||||
assertThat((loadedState.roomMembers as AsyncData.Success).data.invited).isEqualTo(listOf(aVictor(), aWalter()))
|
assertThat(loadedMembersState.roomMembers.invited).isEqualTo(listOf(aVictor(), aWalter()))
|
||||||
assertThat((loadedState.roomMembers as AsyncData.Success).data.joined).isNotEmpty()
|
assertThat(loadedMembersState.roomMembers.joined).isNotEmpty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,6 +88,7 @@ class RoomMemberListPresenterTests {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
val loadedState = awaitItem()
|
val loadedState = awaitItem()
|
||||||
loadedState.eventSink(RoomMemberListEvents.OnSearchActiveChanged(true))
|
loadedState.eventSink(RoomMemberListEvents.OnSearchActiveChanged(true))
|
||||||
|
skipItems(1)
|
||||||
val searchActiveState = awaitItem()
|
val searchActiveState = awaitItem()
|
||||||
assertThat(searchActiveState.isSearchActive).isTrue()
|
assertThat(searchActiveState.isSearchActive).isTrue()
|
||||||
}
|
}
|
||||||
|
|
@ -101,6 +105,7 @@ class RoomMemberListPresenterTests {
|
||||||
loadedState.eventSink(RoomMemberListEvents.OnSearchActiveChanged(true))
|
loadedState.eventSink(RoomMemberListEvents.OnSearchActiveChanged(true))
|
||||||
val searchActiveState = awaitItem()
|
val searchActiveState = awaitItem()
|
||||||
searchActiveState.eventSink(RoomMemberListEvents.UpdateSearchQuery("something"))
|
searchActiveState.eventSink(RoomMemberListEvents.UpdateSearchQuery("something"))
|
||||||
|
skipItems(1)
|
||||||
val searchQueryUpdatedState = awaitItem()
|
val searchQueryUpdatedState = awaitItem()
|
||||||
assertThat(searchQueryUpdatedState.searchQuery).isEqualTo("something")
|
assertThat(searchQueryUpdatedState.searchQuery).isEqualTo("something")
|
||||||
val searchSearchResultDelivered = awaitItem()
|
val searchSearchResultDelivered = awaitItem()
|
||||||
|
|
@ -119,6 +124,7 @@ class RoomMemberListPresenterTests {
|
||||||
loadedState.eventSink(RoomMemberListEvents.OnSearchActiveChanged(true))
|
loadedState.eventSink(RoomMemberListEvents.OnSearchActiveChanged(true))
|
||||||
val searchActiveState = awaitItem()
|
val searchActiveState = awaitItem()
|
||||||
searchActiveState.eventSink(RoomMemberListEvents.UpdateSearchQuery("Alice"))
|
searchActiveState.eventSink(RoomMemberListEvents.UpdateSearchQuery("Alice"))
|
||||||
|
skipItems(1)
|
||||||
val searchQueryUpdatedState = awaitItem()
|
val searchQueryUpdatedState = awaitItem()
|
||||||
assertThat(searchQueryUpdatedState.searchQuery).isEqualTo("Alice")
|
assertThat(searchQueryUpdatedState.searchQuery).isEqualTo("Alice")
|
||||||
val searchSearchResultDelivered = awaitItem()
|
val searchSearchResultDelivered = awaitItem()
|
||||||
|
|
|
||||||
|
|
@ -162,7 +162,8 @@ class RustMatrixRoom(
|
||||||
|
|
||||||
init {
|
init {
|
||||||
timeline.membershipChangeEventReceived
|
timeline.membershipChangeEventReceived
|
||||||
.onEach { roomMemberListFetcher.fetchRoomMembers() }
|
// The new events should already be in the SDK cache, no need to fetch them from the server
|
||||||
|
.onEach { roomMemberListFetcher.fetchRoomMembers(source = RoomMemberListFetcher.Source.CACHE) }
|
||||||
.launchIn(roomCoroutineScope)
|
.launchIn(roomCoroutineScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -219,7 +220,15 @@ class RustMatrixRoom(
|
||||||
override val activeMemberCount: Long
|
override val activeMemberCount: Long
|
||||||
get() = innerRoom.activeMembersCount().toLong()
|
get() = innerRoom.activeMembersCount().toLong()
|
||||||
|
|
||||||
override suspend fun updateMembers() = roomMemberListFetcher.fetchRoomMembers()
|
override suspend fun updateMembers() {
|
||||||
|
val useCache = membersStateFlow.value is MatrixRoomMembersState.Unknown
|
||||||
|
val source = if (useCache) {
|
||||||
|
RoomMemberListFetcher.Source.CACHE_AND_SERVER
|
||||||
|
} else {
|
||||||
|
RoomMemberListFetcher.Source.SERVER
|
||||||
|
}
|
||||||
|
roomMemberListFetcher.fetchRoomMembers(source = source)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun userDisplayName(userId: UserId): Result<String?> = withContext(roomDispatcher) {
|
override suspend fun userDisplayName(userId: UserId): Result<String?> = withContext(roomDispatcher) {
|
||||||
runCatching {
|
runCatching {
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,10 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.impl.room.member
|
package io.element.android.libraries.matrix.impl.room.member
|
||||||
|
|
||||||
import io.element.android.libraries.core.coroutine.parallelMap
|
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.roomMembers
|
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.ensureActive
|
import kotlinx.coroutines.ensureActive
|
||||||
|
|
@ -42,6 +43,11 @@ internal class RoomMemberListFetcher(
|
||||||
private val dispatcher: CoroutineDispatcher,
|
private val dispatcher: CoroutineDispatcher,
|
||||||
private val pageSize: Int = 10_000,
|
private val pageSize: Int = 10_000,
|
||||||
) {
|
) {
|
||||||
|
enum class Source {
|
||||||
|
CACHE,
|
||||||
|
CACHE_AND_SERVER,
|
||||||
|
SERVER,
|
||||||
|
}
|
||||||
private val updatedRoomMemberMutex = Mutex()
|
private val updatedRoomMemberMutex = Mutex()
|
||||||
private val roomId = room.id()
|
private val roomId = room.id()
|
||||||
|
|
||||||
|
|
@ -51,73 +57,86 @@ internal class RoomMemberListFetcher(
|
||||||
/**
|
/**
|
||||||
* Fetches the room members for the given room.
|
* Fetches the room members for the given room.
|
||||||
* It will emit the cached members first, and then the updated members in batches of [pageSize] items, through [membersFlow].
|
* It will emit the cached members first, and then the updated members in batches of [pageSize] items, through [membersFlow].
|
||||||
* @param withCache Whether to load the cached members first. Defaults to true.
|
* @param source Where we should load the members from. Defaults to [Source.CACHE_AND_SERVER].
|
||||||
*/
|
*/
|
||||||
suspend fun fetchRoomMembers(withCache: Boolean = true) {
|
suspend fun fetchRoomMembers(source: Source = Source.CACHE_AND_SERVER) {
|
||||||
if (updatedRoomMemberMutex.isLocked) {
|
if (updatedRoomMemberMutex.isLocked) {
|
||||||
Timber.i("Room members are already being updated for room $roomId")
|
Timber.i("Room members are already being updated for room $roomId")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
updatedRoomMemberMutex.withLock {
|
updatedRoomMemberMutex.withLock {
|
||||||
withContext(dispatcher) {
|
withContext(dispatcher) {
|
||||||
// Load cached members as fallback and to get faster results
|
_membersFlow.run {
|
||||||
if (withCache) {
|
when (source) {
|
||||||
if (_membersFlow.value !is MatrixRoomMembersState.Ready) {
|
Source.CACHE -> {
|
||||||
fetchCachedRoomMembers()
|
fetchCachedRoomMembers(asPendingState = false)
|
||||||
} else {
|
}
|
||||||
Timber.i("Cached members not found for $roomId")
|
Source.CACHE_AND_SERVER -> {
|
||||||
|
fetchCachedRoomMembers(asPendingState = true)
|
||||||
|
fetchRemoteRoomMembers()
|
||||||
|
}
|
||||||
|
Source.SERVER -> {
|
||||||
|
fetchRemoteRoomMembers()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val prevRoomMembers = (_membersFlow.value as? MatrixRoomMembersState.Ready)?.roomMembers?.toImmutableList()
|
|
||||||
_membersFlow.value = MatrixRoomMembersState.Pending(prevRoomMembers = prevRoomMembers)
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Start loading new members
|
|
||||||
parseAndEmitMembers(room.members())
|
|
||||||
} catch (exception: CancellationException) {
|
|
||||||
Timber.d("Cancelled loading updated members for room $roomId")
|
|
||||||
throw exception
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
Timber.e(exception, "Failed to load updated members for room $roomId")
|
|
||||||
_membersFlow.value = MatrixRoomMembersState.Error(exception, prevRoomMembers)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal suspend fun fetchCachedRoomMembers() = withContext(dispatcher) {
|
private suspend fun MutableStateFlow<MatrixRoomMembersState>.fetchCachedRoomMembers(asPendingState: Boolean = true) {
|
||||||
Timber.i("Loading cached members for room $roomId")
|
Timber.i("Loading cached members for room $roomId")
|
||||||
try {
|
try {
|
||||||
val iterator = room.membersNoSync()
|
// Send current member list with pending state to notify the UI that we are loading new members
|
||||||
parseAndEmitMembers(iterator)
|
emit(pendingWithCurrentMembers())
|
||||||
|
val members = parseAndEmitMembers(room.membersNoSync())
|
||||||
|
val newState = if (asPendingState) {
|
||||||
|
MatrixRoomMembersState.Pending(prevRoomMembers = members)
|
||||||
|
} else {
|
||||||
|
MatrixRoomMembersState.Ready(members)
|
||||||
|
}
|
||||||
|
emit(newState)
|
||||||
} catch (exception: CancellationException) {
|
} catch (exception: CancellationException) {
|
||||||
Timber.d("Cancelled loading cached members for room $roomId")
|
Timber.d("Cancelled loading cached members for room $roomId")
|
||||||
throw exception
|
throw exception
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
Timber.e(exception, "Failed to load cached members for room $roomId")
|
Timber.e(exception, "Failed to load cached members for room $roomId")
|
||||||
_membersFlow.value = MatrixRoomMembersState.Error(exception, _membersFlow.value.roomMembers()?.toImmutableList())
|
emit(MatrixRoomMembersState.Error(exception, _membersFlow.value.roomMembers()?.toImmutableList()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun parseAndEmitMembers(roomMembersIterator: RoomMembersIterator) {
|
private suspend fun MutableStateFlow<MatrixRoomMembersState>.fetchRemoteRoomMembers() {
|
||||||
roomMembersIterator.use { iterator ->
|
try {
|
||||||
val results = buildList {
|
// Send current member list with pending state to notify the UI that we are loading new members
|
||||||
|
emit(pendingWithCurrentMembers())
|
||||||
|
// Start loading new members
|
||||||
|
emit(MatrixRoomMembersState.Ready(parseAndEmitMembers(room.members())))
|
||||||
|
} catch (exception: CancellationException) {
|
||||||
|
Timber.d("Cancelled loading updated members for room $roomId")
|
||||||
|
throw exception
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
Timber.e(exception, "Failed to load updated members for room $roomId")
|
||||||
|
emit(MatrixRoomMembersState.Error(exception, _membersFlow.value.roomMembers()?.toImmutableList()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun parseAndEmitMembers(roomMembersIterator: RoomMembersIterator): ImmutableList<RoomMember> {
|
||||||
|
return roomMembersIterator.use { iterator ->
|
||||||
|
val results = buildList(capacity = roomMembersIterator.len().toInt()) {
|
||||||
while (true) {
|
while (true) {
|
||||||
// Loading the whole membersIterator as a stop-gap measure.
|
// Loading the whole membersIterator as a stop-gap measure.
|
||||||
// We should probably implement some sort of paging in the future.
|
// We should probably implement some sort of paging in the future.
|
||||||
coroutineContext.ensureActive()
|
coroutineContext.ensureActive()
|
||||||
val chunk = iterator.nextChunk(pageSize.toUInt())
|
val chunk = iterator.nextChunk(pageSize.toUInt())
|
||||||
// Load next chunk. If null (no more items), exit the loop
|
// Load next chunk. If null (no more items), exit the loop
|
||||||
val members = chunk?.parallelMap(RoomMemberMapper::map) ?: break
|
val members = chunk?.map(RoomMemberMapper::map) ?: break
|
||||||
addAll(members)
|
addAll(members)
|
||||||
Timber.i("Emitting first $size members for room $roomId")
|
Timber.i("Loaded first $size members for room $roomId")
|
||||||
_membersFlow.value = MatrixRoomMembersState.Ready(toImmutableList())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (results.isEmpty()) {
|
results.toImmutableList()
|
||||||
_membersFlow.value = MatrixRoomMembersState.Ready(results.toImmutableList())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun pendingWithCurrentMembers() = MatrixRoomMembersState.Pending(_membersFlow.value.roomMembers().orEmpty().toImmutableList())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,9 @@ import com.google.common.truth.Truth.assertThat
|
||||||
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.MatrixRoomMembersState
|
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||||
import io.element.android.libraries.matrix.api.room.roomMembers
|
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||||
|
import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher.Source.CACHE
|
||||||
|
import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher.Source.CACHE_AND_SERVER
|
||||||
|
import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher.Source.SERVER
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
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
|
||||||
|
|
@ -38,7 +41,7 @@ import uniffi.matrix_sdk.RoomMemberRole
|
||||||
|
|
||||||
class RoomMemberListFetcherTest {
|
class RoomMemberListFetcherTest {
|
||||||
@Test
|
@Test
|
||||||
fun `fetchCachedRoomMembers - emits cached members, if any`() = runTest {
|
fun `fetchRoomMembers with CACHE source - emits cached members, if any`() = runTest {
|
||||||
val room = FakeRustRoom(getMembersNoSync = {
|
val room = FakeRustRoom(getMembersNoSync = {
|
||||||
FakeRoomMembersIterator(
|
FakeRoomMembersIterator(
|
||||||
listOf(
|
listOf(
|
||||||
|
|
@ -53,44 +56,53 @@ class RoomMemberListFetcherTest {
|
||||||
fetcher.membersFlow.test {
|
fetcher.membersFlow.test {
|
||||||
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java)
|
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java)
|
||||||
|
|
||||||
fetcher.fetchCachedRoomMembers()
|
fetcher.fetchRoomMembers(source = CACHE)
|
||||||
|
|
||||||
val readyItem = awaitItem()
|
// Loading state
|
||||||
assertThat(readyItem).isInstanceOf(MatrixRoomMembersState.Ready::class.java)
|
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Pending::class.java)
|
||||||
assertThat((readyItem as? MatrixRoomMembersState.Ready)?.roomMembers?.size).isEqualTo(3)
|
|
||||||
|
val cachedItemsState = awaitItem()
|
||||||
|
assertThat(cachedItemsState).isInstanceOf(MatrixRoomMembersState.Ready::class.java)
|
||||||
|
assertThat((cachedItemsState as? MatrixRoomMembersState.Ready)?.roomMembers).hasSize(3)
|
||||||
|
|
||||||
|
// Assert only the 'no sync' method was called, so no new member sync happened
|
||||||
|
assertThat(room.membersNoSyncCallCount).isEqualTo(1)
|
||||||
|
assertThat(room.membersCallCount).isEqualTo(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `fetchCachedRoomMembers - emits empty list, if no members exist`() = runTest {
|
fun `fetchRoomMembers with CACHE source - emits empty list, if no members exist`() = runTest {
|
||||||
val room = FakeRustRoom(getMembersNoSync = {
|
val room = FakeRustRoom(getMembersNoSync = {
|
||||||
FakeRoomMembersIterator(emptyList())
|
FakeRoomMembersIterator(emptyList())
|
||||||
})
|
})
|
||||||
|
|
||||||
val fetcher = RoomMemberListFetcher(room, Dispatchers.Default)
|
val fetcher = RoomMemberListFetcher(room, Dispatchers.Default)
|
||||||
fetcher.membersFlow.test {
|
fetcher.membersFlow.test {
|
||||||
fetcher.fetchCachedRoomMembers()
|
fetcher.fetchRoomMembers(source = CACHE)
|
||||||
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java)
|
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java)
|
||||||
assertThat(awaitItem().roomMembers()).isEmpty()
|
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Pending::class.java)
|
||||||
|
assertThat((awaitItem() as? MatrixRoomMembersState.Ready)?.roomMembers).isEmpty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `fetchCachedRoomMembers - emits Error on error found`() = runTest {
|
fun `fetchRoomMembers with CACHE source - emits Error on error found`() = runTest {
|
||||||
val room = FakeRustRoom(getMembersNoSync = {
|
val room = FakeRustRoom(getMembersNoSync = {
|
||||||
error("Some unexpected issue")
|
error("Some unexpected issue")
|
||||||
})
|
})
|
||||||
|
|
||||||
val fetcher = RoomMemberListFetcher(room, Dispatchers.Default)
|
val fetcher = RoomMemberListFetcher(room, Dispatchers.Default)
|
||||||
fetcher.membersFlow.test {
|
fetcher.membersFlow.test {
|
||||||
fetcher.fetchCachedRoomMembers()
|
fetcher.fetchRoomMembers(source = CACHE)
|
||||||
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java)
|
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java)
|
||||||
|
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Pending::class.java)
|
||||||
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Error::class.java)
|
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Error::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `fetchCachedRoomMembers - emits items using page size`() = runTest {
|
fun `fetchRoomMembers with CACHE source - emits all items at once`() = runTest {
|
||||||
val room = FakeRustRoom(getMembersNoSync = {
|
val room = FakeRustRoom(getMembersNoSync = {
|
||||||
FakeRoomMembersIterator(
|
FakeRoomMembersIterator(
|
||||||
listOf(
|
listOf(
|
||||||
|
|
@ -103,18 +115,21 @@ class RoomMemberListFetcherTest {
|
||||||
|
|
||||||
val fetcher = RoomMemberListFetcher(room, Dispatchers.Default, pageSize = 2)
|
val fetcher = RoomMemberListFetcher(room, Dispatchers.Default, pageSize = 2)
|
||||||
fetcher.membersFlow.test {
|
fetcher.membersFlow.test {
|
||||||
fetcher.fetchCachedRoomMembers()
|
fetcher.fetchRoomMembers(source = CACHE)
|
||||||
|
|
||||||
|
// Initial state
|
||||||
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java)
|
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java)
|
||||||
assertThat((awaitItem() as? MatrixRoomMembersState.Ready)?.roomMembers?.size).isEqualTo(2)
|
// Started loading cached members
|
||||||
assertThat((awaitItem() as? MatrixRoomMembersState.Ready)?.roomMembers?.size).isEqualTo(3)
|
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Pending::class.java)
|
||||||
|
// Finished loading cached members
|
||||||
|
assertThat((awaitItem() as? MatrixRoomMembersState.Ready)?.roomMembers).hasSize(3)
|
||||||
|
|
||||||
ensureAllEventsConsumed()
|
ensureAllEventsConsumed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `fetchRoomMembers - with 'withCache' set to false emits only new members, if any`() = runTest {
|
fun `fetchRoomMembers with SERVER source - emits only new members, if any`() = runTest {
|
||||||
val room = FakeRustRoom(getMembers = {
|
val room = FakeRustRoom(getMembers = {
|
||||||
FakeRoomMembersIterator(
|
FakeRoomMembersIterator(
|
||||||
listOf(
|
listOf(
|
||||||
|
|
@ -127,21 +142,25 @@ class RoomMemberListFetcherTest {
|
||||||
|
|
||||||
val fetcher = RoomMemberListFetcher(room, Dispatchers.Default)
|
val fetcher = RoomMemberListFetcher(room, Dispatchers.Default)
|
||||||
fetcher.membersFlow.test {
|
fetcher.membersFlow.test {
|
||||||
fetcher.fetchRoomMembers(withCache = false)
|
fetcher.fetchRoomMembers(source = SERVER)
|
||||||
|
|
||||||
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java)
|
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java)
|
||||||
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Pending::class.java)
|
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Pending::class.java)
|
||||||
assertThat((awaitItem() as? MatrixRoomMembersState.Ready)?.roomMembers?.size).isEqualTo(3)
|
assertThat((awaitItem() as? MatrixRoomMembersState.Ready)?.roomMembers?.size).isEqualTo(3)
|
||||||
|
|
||||||
|
// Assert only the 'sync' method was called, so a new member sync happened
|
||||||
|
assertThat(room.membersNoSyncCallCount).isEqualTo(0)
|
||||||
|
assertThat(room.membersCallCount).isEqualTo(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `fetchRoomMembers - on error it emits an Error item`() = runTest {
|
fun `fetchRoomMembers with SERVER source - on error it emits an Error item`() = runTest {
|
||||||
val room = FakeRustRoom(getMembers = { error("An unexpected error") })
|
val room = FakeRustRoom(getMembers = { error("An unexpected error") })
|
||||||
|
|
||||||
val fetcher = RoomMemberListFetcher(room, Dispatchers.Default)
|
val fetcher = RoomMemberListFetcher(room, Dispatchers.Default)
|
||||||
fetcher.membersFlow.test {
|
fetcher.membersFlow.test {
|
||||||
fetcher.fetchRoomMembers(withCache = false)
|
fetcher.fetchRoomMembers(source = SERVER)
|
||||||
|
|
||||||
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java)
|
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java)
|
||||||
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Pending::class.java)
|
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Pending::class.java)
|
||||||
|
|
@ -150,7 +169,7 @@ class RoomMemberListFetcherTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `fetchRoomMembers - with 'withCache' returns cached items first, then new ones`() = runTest {
|
fun `fetchRoomMembers with CACHE_AND_SERVER source - returns cached items first, then new ones`() = runTest {
|
||||||
val room = FakeRustRoom(
|
val room = FakeRustRoom(
|
||||||
getMembersNoSync = {
|
getMembersNoSync = {
|
||||||
FakeRoomMembersIterator(listOf(fakeRustRoomMember(A_USER_ID_4)))
|
FakeRoomMembersIterator(listOf(fakeRustRoomMember(A_USER_ID_4)))
|
||||||
|
|
@ -168,56 +187,28 @@ class RoomMemberListFetcherTest {
|
||||||
|
|
||||||
val fetcher = RoomMemberListFetcher(room, Dispatchers.Default)
|
val fetcher = RoomMemberListFetcher(room, Dispatchers.Default)
|
||||||
fetcher.membersFlow.test {
|
fetcher.membersFlow.test {
|
||||||
fetcher.fetchRoomMembers(withCache = true)
|
fetcher.fetchRoomMembers(source = CACHE_AND_SERVER)
|
||||||
// Initial
|
// Initial
|
||||||
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java)
|
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java)
|
||||||
|
// Loading cached
|
||||||
|
awaitItem().let { pending ->
|
||||||
|
assertThat(pending).isInstanceOf(MatrixRoomMembersState.Pending::class.java)
|
||||||
|
assertThat(pending.roomMembers()).isEmpty()
|
||||||
|
}
|
||||||
// Loaded cached
|
// Loaded cached
|
||||||
awaitItem().let { cached ->
|
awaitItem().let { cached ->
|
||||||
assertThat(cached).isInstanceOf(MatrixRoomMembersState.Ready::class.java)
|
assertThat(cached).isInstanceOf(MatrixRoomMembersState.Pending::class.java)
|
||||||
assertThat(cached.roomMembers()).hasSize(1)
|
assertThat(cached.roomMembers()).hasSize(1)
|
||||||
}
|
}
|
||||||
// Start loading new
|
// Start loading new
|
||||||
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Pending::class.java)
|
|
||||||
awaitItem().let { ready ->
|
awaitItem().let { ready ->
|
||||||
assertThat(ready).isInstanceOf(MatrixRoomMembersState.Ready::class.java)
|
assertThat(ready).isInstanceOf(MatrixRoomMembersState.Ready::class.java)
|
||||||
assertThat(ready.roomMembers()).hasSize(3)
|
assertThat(ready.roomMembers()).hasSize(3)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
// Assert both member methods were called, so both the cache was hit and a new member sync happened
|
||||||
fun `fetchRoomMembers - with 'withCache' skips cache if there is already a ready state`() = runTest {
|
assertThat(room.membersNoSyncCallCount).isEqualTo(1)
|
||||||
val room = FakeRustRoom(
|
assertThat(room.membersCallCount).isEqualTo(1)
|
||||||
getMembersNoSync = {
|
|
||||||
FakeRoomMembersIterator(listOf(fakeRustRoomMember(A_USER_ID_4)))
|
|
||||||
},
|
|
||||||
getMembers = {
|
|
||||||
FakeRoomMembersIterator(
|
|
||||||
listOf(
|
|
||||||
fakeRustRoomMember(A_USER_ID),
|
|
||||||
fakeRustRoomMember(A_USER_ID_2),
|
|
||||||
fakeRustRoomMember(A_USER_ID_3),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
val fetcher = RoomMemberListFetcher(room, Dispatchers.Default)
|
|
||||||
// Set a ready state
|
|
||||||
fetcher.fetchRoomMembers(withCache = false)
|
|
||||||
|
|
||||||
fetcher.membersFlow.test {
|
|
||||||
// Start loading new members
|
|
||||||
fetcher.fetchRoomMembers(withCache = true)
|
|
||||||
// Previous ready state
|
|
||||||
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Ready::class.java)
|
|
||||||
// New pending state
|
|
||||||
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Pending::class.java)
|
|
||||||
// New ready state
|
|
||||||
awaitItem().let { ready ->
|
|
||||||
assertThat(ready).isInstanceOf(MatrixRoomMembersState.Ready::class.java)
|
|
||||||
assertThat(ready.roomMembers()).hasSize(3)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -226,15 +217,20 @@ class FakeRustRoom(
|
||||||
private val getMembers: () -> RoomMembersIterator = { FakeRoomMembersIterator() },
|
private val getMembers: () -> RoomMembersIterator = { FakeRoomMembersIterator() },
|
||||||
private val getMembersNoSync: () -> RoomMembersIterator = { FakeRoomMembersIterator() },
|
private val getMembersNoSync: () -> RoomMembersIterator = { FakeRoomMembersIterator() },
|
||||||
) : Room(NoPointer) {
|
) : Room(NoPointer) {
|
||||||
|
var membersCallCount = 0
|
||||||
|
var membersNoSyncCallCount = 0
|
||||||
|
|
||||||
override fun id(): String {
|
override fun id(): String {
|
||||||
return A_ROOM_ID.value
|
return A_ROOM_ID.value
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun members(): RoomMembersIterator {
|
override suspend fun members(): RoomMembersIterator {
|
||||||
|
membersCallCount++
|
||||||
return getMembers()
|
return getMembers()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun membersNoSync(): RoomMembersIterator {
|
override suspend fun membersNoSync(): RoomMembersIterator {
|
||||||
|
membersNoSyncCallCount++
|
||||||
return getMembersNoSync()
|
return getMembersNoSync()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3e02a3def9e4c0a791f0dbfbed39f2823545a6108e438fe955c43287bcb20c0d
|
oid sha256:6da4563d01a3e9edab952b234d25cb9e366407b1d5ca61410ac3019bd20c405d
|
||||||
size 24028
|
size 35358
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:3e02a3def9e4c0a791f0dbfbed39f2823545a6108e438fe955c43287bcb20c0d
|
||||||
|
size 24028
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:41cf3bf61af52c2c7d71c17fe95f5217cf74a9c930a981131c6736a887edf50b
|
oid sha256:8a30602acc9e9dcfaae3d9245085194abf98db3095d9d407ee9dea35e750f93f
|
||||||
size 22699
|
size 34359
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:41cf3bf61af52c2c7d71c17fe95f5217cf74a9c930a981131c6736a887edf50b
|
||||||
|
size 22699
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:048260d064b0868e1c26c37ff54e260abb2a6ace663061b4c73bc08e2feb030c
|
oid sha256:a9bd17eb1ac6b97db9f72851c81e1c7c45019df2d6c8a019c6f0438fd948e568
|
||||||
size 14874
|
size 13217
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:32e6fb0b216777b77d68c2124550b9a94583986b2f288e52448458064cd4da4e
|
oid sha256:3691742bafd0ac9ec535e99dd733354c5d505c5de4f3d73af313cb01c04035f1
|
||||||
size 14176
|
size 14234
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3c07175544a9e9f498eb1c76507dd67505197fec30d200a7d27eb29bbe01e6fa
|
oid sha256:a9bd17eb1ac6b97db9f72851c81e1c7c45019df2d6c8a019c6f0438fd948e568
|
||||||
size 13158
|
size 13217
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3be57b648bb864bde868b85666b71f6d7fdc4da2fac0c2e3a1e31bd53f51ad12
|
oid sha256:796772eea276c3e9beebd1d752c93b733ddc3aa03ea53dac2402b2255dfd69ab
|
||||||
size 13960
|
size 12364
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:32e56921168421f50abfbc2456907263b88de478e04c126419648b6939766c67
|
oid sha256:02f80100d1e6fa388c04dba22842189489510969be3e1af13cc500226b8fde93
|
||||||
size 13184
|
size 13232
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6cab4067a59e8e0043df3b68b422279f34c2f072a4c4ebf2aa11ce92a26dd2ce
|
oid sha256:796772eea276c3e9beebd1d752c93b733ddc3aa03ea53dac2402b2255dfd69ab
|
||||||
size 12315
|
size 12364
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue