Do not display empty room list state before the loading one (#2402)
* Do not display empty room list state before the loading one
This commit is contained in:
parent
dbc442e239
commit
e7ed466b4f
10 changed files with 41 additions and 39 deletions
|
|
@ -0,0 +1 @@
|
||||||
|
Do not display empty room list state before the loading one when we still don't have any items
|
||||||
|
|
@ -58,7 +58,7 @@ class InviteListPresenter @Inject constructor(
|
||||||
.roomListService
|
.roomListService
|
||||||
.invites
|
.invites
|
||||||
.summaries
|
.summaries
|
||||||
.collectAsState()
|
.collectAsState(initial = emptyList())
|
||||||
|
|
||||||
var seenInvites by remember { mutableStateOf<Set<RoomId>>(emptySet()) }
|
var seenInvites by remember { mutableStateOf<Set<RoomId>>(emptySet()) }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package io.element.android.features.invitelist.impl
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionMode
|
import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
|
import app.cash.turbine.TurbineTestContext
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.invitelist.api.SeenInvitesStore
|
import io.element.android.features.invitelist.api.SeenInvitesStore
|
||||||
|
|
@ -83,7 +84,7 @@ class InviteListPresenterTests {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val withInviteState = awaitItem()
|
val withInviteState = awaitInitialItem()
|
||||||
assertThat(withInviteState.inviteList.size).isEqualTo(1)
|
assertThat(withInviteState.inviteList.size).isEqualTo(1)
|
||||||
assertThat(withInviteState.inviteList[0].roomId).isEqualTo(A_ROOM_ID)
|
assertThat(withInviteState.inviteList[0].roomId).isEqualTo(A_ROOM_ID)
|
||||||
assertThat(withInviteState.inviteList[0].roomAlias).isEqualTo(A_USER_ID.value)
|
assertThat(withInviteState.inviteList[0].roomAlias).isEqualTo(A_USER_ID.value)
|
||||||
|
|
@ -109,7 +110,7 @@ class InviteListPresenterTests {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val withInviteState = awaitItem()
|
val withInviteState = awaitInitialItem()
|
||||||
assertThat(withInviteState.inviteList.size).isEqualTo(1)
|
assertThat(withInviteState.inviteList.size).isEqualTo(1)
|
||||||
assertThat(withInviteState.inviteList[0].sender?.displayName).isEqualTo(A_USER_NAME)
|
assertThat(withInviteState.inviteList[0].sender?.displayName).isEqualTo(A_USER_NAME)
|
||||||
assertThat(withInviteState.inviteList[0].sender?.userId).isEqualTo(A_USER_ID)
|
assertThat(withInviteState.inviteList[0].sender?.userId).isEqualTo(A_USER_ID)
|
||||||
|
|
@ -138,7 +139,7 @@ class InviteListPresenterTests {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val originalState = awaitItem()
|
val originalState = awaitInitialItem()
|
||||||
originalState.eventSink(InviteListEvents.DeclineInvite(originalState.inviteList[0]))
|
originalState.eventSink(InviteListEvents.DeclineInvite(originalState.inviteList[0]))
|
||||||
|
|
||||||
val newState = awaitItem()
|
val newState = awaitItem()
|
||||||
|
|
@ -159,7 +160,7 @@ class InviteListPresenterTests {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val originalState = awaitItem()
|
val originalState = awaitInitialItem()
|
||||||
originalState.eventSink(InviteListEvents.DeclineInvite(originalState.inviteList[0]))
|
originalState.eventSink(InviteListEvents.DeclineInvite(originalState.inviteList[0]))
|
||||||
|
|
||||||
val newState = awaitItem()
|
val newState = awaitItem()
|
||||||
|
|
@ -180,7 +181,7 @@ class InviteListPresenterTests {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val originalState = awaitItem()
|
val originalState = awaitInitialItem()
|
||||||
originalState.eventSink(InviteListEvents.DeclineInvite(originalState.inviteList[0]))
|
originalState.eventSink(InviteListEvents.DeclineInvite(originalState.inviteList[0]))
|
||||||
|
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
@ -206,7 +207,7 @@ class InviteListPresenterTests {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val originalState = awaitItem()
|
val originalState = awaitInitialItem()
|
||||||
originalState.eventSink(InviteListEvents.DeclineInvite(originalState.inviteList[0]))
|
originalState.eventSink(InviteListEvents.DeclineInvite(originalState.inviteList[0]))
|
||||||
|
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
@ -234,7 +235,7 @@ class InviteListPresenterTests {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val originalState = awaitItem()
|
val originalState = awaitInitialItem()
|
||||||
originalState.eventSink(InviteListEvents.DeclineInvite(originalState.inviteList[0]))
|
originalState.eventSink(InviteListEvents.DeclineInvite(originalState.inviteList[0]))
|
||||||
|
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
@ -264,7 +265,7 @@ class InviteListPresenterTests {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val originalState = awaitItem()
|
val originalState = awaitInitialItem()
|
||||||
originalState.eventSink(InviteListEvents.DeclineInvite(originalState.inviteList[0]))
|
originalState.eventSink(InviteListEvents.DeclineInvite(originalState.inviteList[0]))
|
||||||
|
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
@ -295,7 +296,7 @@ class InviteListPresenterTests {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val originalState = awaitItem()
|
val originalState = awaitInitialItem()
|
||||||
originalState.eventSink(InviteListEvents.AcceptInvite(originalState.inviteList[0]))
|
originalState.eventSink(InviteListEvents.AcceptInvite(originalState.inviteList[0]))
|
||||||
|
|
||||||
val newState = awaitItem()
|
val newState = awaitItem()
|
||||||
|
|
@ -320,7 +321,7 @@ class InviteListPresenterTests {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val originalState = awaitItem()
|
val originalState = awaitInitialItem()
|
||||||
originalState.eventSink(InviteListEvents.AcceptInvite(originalState.inviteList[0]))
|
originalState.eventSink(InviteListEvents.AcceptInvite(originalState.inviteList[0]))
|
||||||
|
|
||||||
assertThat(awaitItem().acceptedAction).isEqualTo(AsyncData.Failure<RoomId>(ex))
|
assertThat(awaitItem().acceptedAction).isEqualTo(AsyncData.Failure<RoomId>(ex))
|
||||||
|
|
@ -342,7 +343,7 @@ class InviteListPresenterTests {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val originalState = awaitItem()
|
val originalState = awaitInitialItem()
|
||||||
originalState.eventSink(InviteListEvents.AcceptInvite(originalState.inviteList[0]))
|
originalState.eventSink(InviteListEvents.AcceptInvite(originalState.inviteList[0]))
|
||||||
|
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
@ -485,6 +486,11 @@ class InviteListPresenterTests {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private suspend fun TurbineTestContext<InviteListState>.awaitInitialItem(): InviteListState {
|
||||||
|
skipItems(1)
|
||||||
|
return awaitItem()
|
||||||
|
}
|
||||||
|
|
||||||
private fun createPresenter(
|
private fun createPresenter(
|
||||||
client: MatrixClient,
|
client: MatrixClient,
|
||||||
seenInvitesStore: SeenInvitesStore = FakeSeenInvitesStore(),
|
seenInvitesStore: SeenInvitesStore = FakeSeenInvitesStore(),
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ class DefaultInviteStateDataSource @Inject constructor(
|
||||||
.roomListService
|
.roomListService
|
||||||
.invites
|
.invites
|
||||||
.summaries
|
.summaries
|
||||||
.collectAsState()
|
.collectAsState(initial = emptyList())
|
||||||
|
|
||||||
val seenInvites by seenInvitesStore
|
val seenInvites by seenInvitesStore
|
||||||
.seenRoomIds()
|
.seenRoomIds()
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ import io.element.android.libraries.androidutils.diff.DiffCacheUpdater
|
||||||
import io.element.android.libraries.androidutils.diff.MutableListDiffCache
|
import io.element.android.libraries.androidutils.diff.MutableListDiffCache
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
|
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomList
|
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
@ -33,6 +32,7 @@ import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.onStart
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
@ -62,6 +62,12 @@ class RoomListDataSource @Inject constructor(
|
||||||
roomListService
|
roomListService
|
||||||
.allRooms
|
.allRooms
|
||||||
.summaries
|
.summaries
|
||||||
|
.onStart {
|
||||||
|
// If we have no cached results, display a placeholder loading state
|
||||||
|
if (diffCache.isEmpty()) {
|
||||||
|
_allRooms.emit(RoomListRoomSummaryFactory.createFakeList())
|
||||||
|
}
|
||||||
|
}
|
||||||
.onEach { roomSummaries ->
|
.onEach { roomSummaries ->
|
||||||
replaceWith(roomSummaries)
|
replaceWith(roomSummaries)
|
||||||
}
|
}
|
||||||
|
|
@ -88,23 +94,10 @@ class RoomListDataSource @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun buildAndEmitAllRooms(roomSummaries: List<RoomSummary>) {
|
private suspend fun buildAndEmitAllRooms(roomSummaries: List<RoomSummary>) {
|
||||||
if (diffCache.isEmpty() && roomListService.allRooms.loadingState.value is RoomList.LoadingState.NotLoaded) {
|
val roomListRoomSummaries = diffCache.indices().mapNotNull { index ->
|
||||||
// If the room list is not loaded, we emit a fake placeholders list
|
diffCache.get(index) ?: buildAndCacheItem(roomSummaries, index)
|
||||||
_allRooms.emit(RoomListRoomSummaryFactory.createFakeList())
|
|
||||||
} else {
|
|
||||||
val roomListRoomSummaries = ArrayList<RoomListRoomSummary>()
|
|
||||||
for (index in diffCache.indices()) {
|
|
||||||
val cacheItem = diffCache.get(index)
|
|
||||||
if (cacheItem == null) {
|
|
||||||
buildAndCacheItem(roomSummaries, index)?.also { timelineItemState ->
|
|
||||||
roomListRoomSummaries.add(timelineItemState)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
roomListRoomSummaries.add(cacheItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_allRooms.emit(roomListRoomSummaries.toImmutableList())
|
|
||||||
}
|
}
|
||||||
|
_allRooms.emit(roomListRoomSummaries.toImmutableList())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildAndCacheItem(roomSummaries: List<RoomSummary>, index: Int): RoomListRoomSummary? {
|
private fun buildAndCacheItem(roomSummaries: List<RoomSummary>, index: Int): RoomListRoomSummary? {
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ internal class DefaultInviteStateDataSourceTest {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
dataSource.inviteState()
|
dataSource.inviteState()
|
||||||
}.test {
|
}.test {
|
||||||
skipItems(1)
|
skipItems(2)
|
||||||
assertThat(awaitItem()).isEqualTo(InvitesState.NewInvites)
|
assertThat(awaitItem()).isEqualTo(InvitesState.NewInvites)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package io.element.android.libraries.matrix.api.roomlist
|
package io.element.android.libraries.matrix.api.roomlist
|
||||||
|
|
||||||
import kotlinx.coroutines.TimeoutCancellationException
|
import kotlinx.coroutines.TimeoutCancellationException
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import kotlinx.coroutines.withTimeout
|
import kotlinx.coroutines.withTimeout
|
||||||
|
|
@ -51,7 +52,7 @@ interface RoomList {
|
||||||
/**
|
/**
|
||||||
* The list of room summaries as a flow.
|
* The list of room summaries as a flow.
|
||||||
*/
|
*/
|
||||||
val summaries: StateFlow<List<RoomSummary>>
|
val summaries: SharedFlow<List<RoomSummary>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The loading state of the room list as a flow.
|
* The loading state of the room list as a flow.
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ internal class RoomListFactory(
|
||||||
innerProvider: suspend () -> InnerRoomList
|
innerProvider: suspend () -> InnerRoomList
|
||||||
): DynamicRoomList {
|
): DynamicRoomList {
|
||||||
val loadingStateFlow: MutableStateFlow<RoomList.LoadingState> = MutableStateFlow(RoomList.LoadingState.NotLoaded)
|
val loadingStateFlow: MutableStateFlow<RoomList.LoadingState> = MutableStateFlow(RoomList.LoadingState.NotLoaded)
|
||||||
val summariesFlow = MutableStateFlow<List<RoomSummary>>(emptyList())
|
val summariesFlow = MutableSharedFlow<List<RoomSummary>>(replay = 1, extraBufferCapacity = 1)
|
||||||
val processor = RoomSummaryListProcessor(summariesFlow, innerRoomListService, coroutineContext, roomSummaryDetailsFactory)
|
val processor = RoomSummaryListProcessor(summariesFlow, innerRoomListService, coroutineContext, roomSummaryDetailsFactory)
|
||||||
// Makes sure we don't miss any events
|
// Makes sure we don't miss any events
|
||||||
val dynamicEvents = MutableSharedFlow<RoomListDynamicEvents>(replay = 100)
|
val dynamicEvents = MutableSharedFlow<RoomListDynamicEvents>(replay = 100)
|
||||||
|
|
@ -92,7 +92,7 @@ internal class RoomListFactory(
|
||||||
}
|
}
|
||||||
|
|
||||||
private class RustDynamicRoomList(
|
private class RustDynamicRoomList(
|
||||||
override val summaries: MutableStateFlow<List<RoomSummary>>,
|
override val summaries: MutableSharedFlow<List<RoomSummary>>,
|
||||||
override val loadingState: MutableStateFlow<RoomList.LoadingState>,
|
override val loadingState: MutableStateFlow<RoomList.LoadingState>,
|
||||||
override val currentFilter: MutableStateFlow<RoomListFilter>,
|
override val currentFilter: MutableStateFlow<RoomListFilter>,
|
||||||
override val loadedPages: MutableStateFlow<Int>,
|
override val loadedPages: MutableStateFlow<Int>,
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
package io.element.android.libraries.matrix.impl.roomlist
|
package io.element.android.libraries.matrix.impl.roomlist
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
@ -30,7 +30,7 @@ import java.util.UUID
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
class RoomSummaryListProcessor(
|
class RoomSummaryListProcessor(
|
||||||
private val roomSummaries: MutableStateFlow<List<RoomSummary>>,
|
private val roomSummaries: MutableSharedFlow<List<RoomSummary>>,
|
||||||
private val roomListService: RoomListServiceInterface,
|
private val roomListService: RoomListServiceInterface,
|
||||||
private val coroutineContext: CoroutineContext,
|
private val coroutineContext: CoroutineContext,
|
||||||
private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
|
private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
|
||||||
|
|
@ -132,9 +132,10 @@ class RoomSummaryListProcessor(
|
||||||
|
|
||||||
private suspend fun updateRoomSummaries(block: suspend MutableList<RoomSummary>.() -> Unit) = withContext(coroutineContext) {
|
private suspend fun updateRoomSummaries(block: suspend MutableList<RoomSummary>.() -> Unit) = withContext(coroutineContext) {
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
val mutableRoomSummaries = roomSummaries.value.toMutableList()
|
val current = roomSummaries.replayCache.lastOrNull()
|
||||||
|
val mutableRoomSummaries = current.orEmpty().toMutableList()
|
||||||
block(mutableRoomSummaries)
|
block(mutableRoomSummaries)
|
||||||
roomSummaries.value = mutableRoomSummaries
|
roomSummaries.emit(mutableRoomSummaries)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ class RoomSelectPresenter @AssistedInject constructor(
|
||||||
var isSearchActive by remember { mutableStateOf(false) }
|
var isSearchActive by remember { mutableStateOf(false) }
|
||||||
var results: SearchBarResultState<ImmutableList<RoomSummaryDetails>> by remember { mutableStateOf(SearchBarResultState.Initial()) }
|
var results: SearchBarResultState<ImmutableList<RoomSummaryDetails>> by remember { mutableStateOf(SearchBarResultState.Initial()) }
|
||||||
|
|
||||||
val summaries by client.roomListService.allRooms.summaries.collectAsState()
|
val summaries by client.roomListService.allRooms.summaries.collectAsState(initial = emptyList())
|
||||||
|
|
||||||
LaunchedEffect(query, summaries) {
|
LaunchedEffect(query, summaries) {
|
||||||
val filteredSummaries = summaries.filterIsInstance<RoomSummary.Filled>()
|
val filteredSummaries = summaries.filterIsInstance<RoomSummary.Filled>()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue