RoomList: use same logic than Timeline for caching built items. (#1013)
* RoomList: use same logic than Timeline for caching built items. Extract into reusable components. * RoomList: fix tests * Fix `DiffCacheUpdater` docs --------- Co-authored-by: ganfra <francoisg@element.io> Co-authored-by: Jorge Martín <jorgem@element.io>
This commit is contained in:
parent
89b1bba96e
commit
e453b984ef
11 changed files with 373 additions and 142 deletions
|
|
@ -18,6 +18,8 @@ package io.element.android.features.roomlist.impl.datasource
|
|||
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryPlaceholders
|
||||
import io.element.android.libraries.androidutils.diff.DiffCacheUpdater
|
||||
import io.element.android.libraries.androidutils.diff.MutableListDiffCache
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.extensions.orEmpty
|
||||
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
|
||||
|
|
@ -36,6 +38,8 @@ import kotlinx.coroutines.flow.StateFlow
|
|||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -50,15 +54,17 @@ class RoomListDataSource @Inject constructor(
|
|||
private val _allRooms = MutableStateFlow<ImmutableList<RoomListRoomSummary>>(persistentListOf())
|
||||
private val _filteredRooms = MutableStateFlow<ImmutableList<RoomListRoomSummary>>(persistentListOf())
|
||||
|
||||
private val lock = Mutex()
|
||||
private val diffCache = MutableListDiffCache<RoomListRoomSummary>()
|
||||
private val diffCacheUpdater = DiffCacheUpdater<RoomSummary, RoomListRoomSummary>(diffCache = diffCache, detectMoves = true) { old, new ->
|
||||
old?.identifier() == new?.identifier()
|
||||
}
|
||||
|
||||
fun launchIn(coroutineScope: CoroutineScope) {
|
||||
roomSummaryDataSource
|
||||
.allRooms()
|
||||
.onEach { roomSummaries ->
|
||||
_allRooms.value = if (roomSummaries.isEmpty()) {
|
||||
RoomListRoomSummaryPlaceholders.createFakeList(16)
|
||||
} else {
|
||||
mapRoomSummaries(roomSummaries)
|
||||
}.toImmutableList()
|
||||
replaceWith(roomSummaries)
|
||||
}
|
||||
.launchIn(coroutineScope)
|
||||
|
||||
|
|
@ -85,33 +91,63 @@ class RoomListDataSource @Inject constructor(
|
|||
val allRooms: StateFlow<ImmutableList<RoomListRoomSummary>> = _allRooms
|
||||
val filteredRooms: StateFlow<ImmutableList<RoomListRoomSummary>> = _filteredRooms
|
||||
|
||||
private suspend fun mapRoomSummaries(
|
||||
roomSummaries: List<RoomSummary>
|
||||
): List<RoomListRoomSummary> = withContext(coroutineDispatchers.computation) {
|
||||
roomSummaries.map { roomSummary ->
|
||||
when (roomSummary) {
|
||||
is RoomSummary.Empty -> RoomListRoomSummaryPlaceholders.create(roomSummary.identifier)
|
||||
is RoomSummary.Filled -> {
|
||||
val avatarData = AvatarData(
|
||||
id = roomSummary.identifier(),
|
||||
name = roomSummary.details.name,
|
||||
url = roomSummary.details.avatarURLString,
|
||||
size = AvatarSize.RoomListItem,
|
||||
)
|
||||
val roomIdentifier = roomSummary.identifier()
|
||||
RoomListRoomSummary(
|
||||
id = roomSummary.identifier(),
|
||||
roomId = RoomId(roomIdentifier),
|
||||
name = roomSummary.details.name,
|
||||
hasUnread = roomSummary.details.unreadNotificationCount > 0,
|
||||
timestamp = lastMessageTimestampFormatter.format(roomSummary.details.lastMessageTimestamp),
|
||||
lastMessage = roomSummary.details.lastMessage?.let { message ->
|
||||
roomLastMessageFormatter.format(message.event, roomSummary.details.isDirect)
|
||||
}.orEmpty(),
|
||||
avatarData = avatarData,
|
||||
)
|
||||
}
|
||||
}
|
||||
private suspend fun replaceWith(roomSummaries: List<RoomSummary>) = withContext(coroutineDispatchers.computation) {
|
||||
lock.withLock {
|
||||
diffCacheUpdater.updateWith(roomSummaries)
|
||||
buildAndEmitAllRooms(roomSummaries)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun buildAndEmitAllRooms(roomSummaries: List<RoomSummary>) {
|
||||
if (diffCache.isEmpty()) {
|
||||
_allRooms.emit(
|
||||
RoomListRoomSummaryPlaceholders.createFakeList(16).toImmutableList()
|
||||
)
|
||||
} 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())
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildAndCacheItem(
|
||||
roomSummaries: List<RoomSummary>,
|
||||
index: Int
|
||||
): RoomListRoomSummary? {
|
||||
val roomListRoomSummary = when (val roomSummary = roomSummaries.getOrNull(index)) {
|
||||
is RoomSummary.Empty -> RoomListRoomSummaryPlaceholders.create(roomSummary.identifier)
|
||||
is RoomSummary.Filled -> {
|
||||
val avatarData = AvatarData(
|
||||
id = roomSummary.identifier(),
|
||||
name = roomSummary.details.name,
|
||||
url = roomSummary.details.avatarURLString,
|
||||
size = AvatarSize.RoomListItem,
|
||||
)
|
||||
val roomIdentifier = roomSummary.identifier()
|
||||
RoomListRoomSummary(
|
||||
id = roomSummary.identifier(),
|
||||
roomId = RoomId(roomIdentifier),
|
||||
name = roomSummary.details.name,
|
||||
hasUnread = roomSummary.details.unreadNotificationCount > 0,
|
||||
timestamp = lastMessageTimestampFormatter.format(roomSummary.details.lastMessageTimestamp),
|
||||
lastMessage = roomSummary.details.lastMessage?.let { message ->
|
||||
roomLastMessageFormatter.format(message.event, roomSummary.details.isDirect)
|
||||
}.orEmpty(),
|
||||
avatarData = avatarData,
|
||||
)
|
||||
}
|
||||
null -> null
|
||||
}
|
||||
diffCache[index] = roomListRoomSummary
|
||||
return roomListRoomSummary
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient
|
|||
import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource
|
||||
import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled
|
||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
|
|
@ -118,13 +119,12 @@ class RoomListPresenterTests {
|
|||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
val initialState = consumeItemsUntilPredicate { state -> state.roomList.size == 16 }.last()
|
||||
// Room list is loaded with 16 placeholders
|
||||
Truth.assertThat(initialState.roomList.size).isEqualTo(16)
|
||||
Truth.assertThat(initialState.roomList.all { it.isPlaceholder }).isTrue()
|
||||
roomSummaryDataSource.postAllRooms(listOf(aRoomSummaryFilled()))
|
||||
val withRoomState = awaitItem()
|
||||
val withRoomState = consumeItemsUntilPredicate { state -> state.roomList.size == 1 }.last()
|
||||
Truth.assertThat(withRoomState.roomList.size).isEqualTo(1)
|
||||
Truth.assertThat(withRoomState.roomList.first())
|
||||
.isEqualTo(aRoomListRoomSummary)
|
||||
|
|
@ -142,21 +142,19 @@ class RoomListPresenterTests {
|
|||
presenter.present()
|
||||
}.test {
|
||||
roomSummaryDataSource.postAllRooms(listOf(aRoomSummaryFilled()))
|
||||
skipItems(1)
|
||||
val loadedState = awaitItem()
|
||||
val loadedState = consumeItemsUntilPredicate { state -> state.roomList.size == 1 }.last()
|
||||
// Test filtering with result
|
||||
loadedState.eventSink.invoke(RoomListEvents.UpdateFilter(A_ROOM_NAME.substring(0, 3)))
|
||||
skipItems(1) // Filter update
|
||||
val withNotFilteredRoomState = awaitItem()
|
||||
Truth.assertThat(withNotFilteredRoomState.filter).isEqualTo(A_ROOM_NAME.substring(0, 3))
|
||||
Truth.assertThat(withNotFilteredRoomState.filteredRoomList.size).isEqualTo(1)
|
||||
Truth.assertThat(withNotFilteredRoomState.filteredRoomList.first())
|
||||
val withFilteredRoomState = consumeItemsUntilPredicate { state -> state.filteredRoomList.size == 1 }.last()
|
||||
Truth.assertThat(withFilteredRoomState.filter).isEqualTo(A_ROOM_NAME.substring(0, 3))
|
||||
Truth.assertThat(withFilteredRoomState.filteredRoomList.size).isEqualTo(1)
|
||||
Truth.assertThat(withFilteredRoomState.filteredRoomList.first())
|
||||
.isEqualTo(aRoomListRoomSummary)
|
||||
// Test filtering without result
|
||||
withNotFilteredRoomState.eventSink.invoke(RoomListEvents.UpdateFilter("tada"))
|
||||
skipItems(1) // Filter update
|
||||
Truth.assertThat(awaitItem().filter).isEqualTo("tada")
|
||||
Truth.assertThat(awaitItem().filteredRoomList).isEmpty()
|
||||
withFilteredRoomState.eventSink.invoke(RoomListEvents.UpdateFilter("tada"))
|
||||
val withNotFilteredRoomState = consumeItemsUntilPredicate { state -> state.filteredRoomList.size == 0 }.last()
|
||||
Truth.assertThat(withNotFilteredRoomState.filter).isEqualTo("tada")
|
||||
Truth.assertThat(withNotFilteredRoomState.filteredRoomList).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue