Merge pull request #1865 from vector-im/feature/fga/room_list_dynamic_api

RoomList : rework a bit the api and make usage of entriesWithDynamicAdapter
This commit is contained in:
ganfra
2023-11-23 12:07:55 +01:00
committed by GitHub
17 changed files with 317 additions and 213 deletions

View File

@@ -57,7 +57,7 @@ class InviteListPresenter @Inject constructor(
override fun present(): InviteListState {
val invites by client
.roomListService
.invites()
.invites
.summaries
.collectAsState()

View File

@@ -65,7 +65,7 @@ class ForwardMessagesPresenter @AssistedInject constructor(
var results: SearchBarResultState<ImmutableList<RoomSummaryDetails>> by remember { mutableStateOf(SearchBarResultState.NotSearching()) }
val forwardingActionState: MutableState<Async<ImmutableList<RoomId>>> = remember { mutableStateOf(Async.Uninitialized) }
val summaries by client.roomListService.allRooms().summaries.collectAsState()
val summaries by client.roomListService.allRooms.summaries.collectAsState()
LaunchedEffect(query, summaries) {
val filteredSummaries = summaries.filterIsInstance<RoomSummary.Filled>()

View File

@@ -105,7 +105,7 @@ class EditDefaultNotificationSettingPresenter @AssistedInject constructor(
}
private fun CoroutineScope.observeRoomSummaries(roomsWithUserDefinedMode: MutableState<List<RoomSummary.Filled>>) {
roomListService.allRooms()
roomListService.allRooms
.summaries
.onEach {
updateRoomsWithUserDefinedMode(it, roomsWithUserDefinedMode)

View File

@@ -45,7 +45,7 @@ class DefaultInviteStateDataSource @Inject constructor(
override fun inviteState(): InvitesState {
val invites by client
.roomListService
.invites()
.invites
.summaries
.collectAsState()

View File

@@ -71,7 +71,7 @@ class RoomListDataSource @Inject constructor(
fun launchIn(coroutineScope: CoroutineScope) {
roomListService
.allRooms()
.allRooms
.summaries
.onEach { roomSummaries ->
replaceWith(roomSummaries)
@@ -106,7 +106,7 @@ class RoomListDataSource @Inject constructor(
notificationSettingsService.notificationSettingsChangeFlow
.debounce(0.5.seconds)
.onEach {
roomListService.rebuildRoomSummaries()
roomListService.allRooms.rebuildSummaries()
}
.launchIn(appScope)
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.api.roomlist
/**
* RoomList with dynamic filtering and loading.
* This is useful for large lists of rooms.
* It lets load rooms on demand and filter them.
*/
interface DynamicRoomList : RoomList {
companion object {
const val DEFAULT_PAGE_SIZE = 20
const val DEFAULT_PAGES_TO_LOAD = 10
}
sealed interface Filter {
/**
* No filter applied.
*/
data object All : Filter
/**
* Filter all rooms.
*/
data object None : Filter
/**
* Filter rooms by normalized room name.
*/
data class NormalizedMatchRoomName(val pattern: String) : Filter
}
/**
* Load more rooms into the list if possible.
*/
suspend fun loadMore()
/**
* Reset the list to its initial size.
*/
suspend fun reset()
/**
* Update the filter to apply to the list.
* @param filter the filter to apply.
*/
suspend fun updateFilter(filter: Filter)
}

View File

@@ -28,6 +28,7 @@ import kotlin.time.Duration
* Can be retrieved from [RoomListService] methods.
*/
interface RoomList {
sealed interface LoadingState {
data object NotLoaded : LoadingState
data class Loaded(val numberOfRooms: Int) : LoadingState
@@ -43,6 +44,12 @@ interface RoomList {
* This is useful to know if a specific set of rooms is loaded or not.
*/
val loadingState: StateFlow<LoadingState>
/**
* Force a refresh of the room summaries.
* Might be useful for some situations where we are not notified of changes.
*/
suspend fun rebuildSummaries()
}
suspend fun RoomList.awaitLoaded(timeout: Duration = Duration.INFINITE) {

View File

@@ -41,12 +41,12 @@ interface RoomListService {
* returns a [RoomList] object of all rooms we want to display.
* This will exclude some rooms like the invites, or spaces.
*/
fun allRooms(): RoomList
val allRooms: RoomList
/**
* returns a [RoomList] object of all invites.
*/
fun invites(): RoomList
val invites: RoomList
/**
* Will set the visible range of all rooms.
@@ -54,11 +54,6 @@ interface RoomListService {
*/
fun updateAllRoomsVisibleRange(range: IntRange)
/**
* Rebuild the room summaries, required when we know some data may have changed. (E.g. room notification settings)
*/
fun rebuildRoomSummaries()
/**
* The sync indicator as a flow.
*/

View File

@@ -53,6 +53,7 @@ import io.element.android.libraries.matrix.impl.room.MatrixRoomInfoMapper
import io.element.android.libraries.matrix.impl.room.RoomContentForwarder
import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber
import io.element.android.libraries.matrix.impl.room.RustMatrixRoom
import io.element.android.libraries.matrix.impl.roomlist.RoomListFactory
import io.element.android.libraries.matrix.impl.roomlist.RustRoomListService
import io.element.android.libraries.matrix.impl.roomlist.roomOrNull
import io.element.android.libraries.matrix.impl.sync.RustSyncService
@@ -171,7 +172,11 @@ class RustMatrixClient constructor(
RustRoomListService(
innerRoomListService = innerRoomListService,
sessionCoroutineScope = sessionCoroutineScope,
dispatcher = sessionDispatcher,
roomListFactory = RoomListFactory(
innerRoomListService = innerRoomListService,
coroutineScope = sessionCoroutineScope,
dispatcher = sessionDispatcher,
),
)
override val roomListService: RoomListService
@@ -200,7 +205,7 @@ class RustMatrixClient constructor(
var cachedPairOfRoom = pairOfRoom(roomId)
if (cachedPairOfRoom == null) {
//... otherwise, lets wait for the SS to load all rooms and check again.
roomListService.allRooms().awaitLoaded()
roomListService.allRooms.awaitLoaded()
cachedPairOfRoom = pairOfRoom(roomId)
}
cachedPairOfRoom?.let { (roomListItem, fullRoom) ->
@@ -274,7 +279,7 @@ class RustMatrixClient constructor(
// Wait to receive the room back from the sync
withTimeout(30_000L) {
roomListService.allRooms().summaries
roomListService.allRooms.summaries
.filter { roomSummaries ->
roomSummaries.map { it.identifier() }.contains(roomId.value)
}

View File

@@ -16,14 +16,10 @@
package io.element.android.libraries.matrix.impl.roomlist
import io.element.android.libraries.matrix.api.roomlist.RoomList
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.coroutines.flow.StateFlow
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind
/**
* Simple implementation of [RoomList] where state flows are provided through constructor.
*/
class RustRoomList(
override val summaries: StateFlow<List<RoomSummary>>,
override val loadingState: StateFlow<RoomList.LoadingState>
) : RoomList
internal sealed interface RoomListDynamicEvents {
data object Reset : RoomListDynamicEvents
data object LoadMore : RoomListDynamicEvents
data class SetFilter(val filter: RoomListEntriesDynamicFilterKind) : RoomListDynamicEvents
}

View File

@@ -17,15 +17,20 @@
package io.element.android.libraries.matrix.impl.roomlist
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind
import org.matrix.rustcomponents.sdk.RoomListEntriesListener
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
import org.matrix.rustcomponents.sdk.RoomListEntry
import org.matrix.rustcomponents.sdk.RoomListInterface
import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.RoomListLoadingState
@@ -58,25 +63,45 @@ fun RoomListInterface.loadingStateFlow(): Flow<RoomListLoadingState> =
Timber.d(it, "loadingStateFlow() failed")
}.buffer(Channel.UNLIMITED)
fun RoomListInterface.entriesFlow(onInitialList: suspend (List<RoomListEntry>) -> Unit): Flow<List<RoomListEntriesUpdate>> =
mxCallbackFlow {
internal fun RoomListInterface.entriesFlow(
pageSize: Int,
numberOfPages: Int,
roomListDynamicEvents: Flow<RoomListDynamicEvents>,
initialFilterKind: RoomListEntriesDynamicFilterKind
): Flow<List<RoomListEntriesUpdate>> =
callbackFlow {
val listener = object : RoomListEntriesListener {
override fun onUpdate(roomEntriesUpdate: List<RoomListEntriesUpdate>) {
trySendBlocking(roomEntriesUpdate)
}
}
val result = entries(listener)
try {
onInitialList(result.entries)
} catch (exception: Exception) {
Timber.d("entriesFlow() onInitialList failed.")
val result = entriesWithDynamicAdapters(pageSize.toUInt(), listener)
val controller = result.controller
controller.setFilter(initialFilterKind)
roomListDynamicEvents.onEach { controllerEvents ->
when (controllerEvents) {
is RoomListDynamicEvents.SetFilter -> {
controller.setFilter(controllerEvents.filter)
}
is RoomListDynamicEvents.LoadMore -> {
repeat(numberOfPages) {
controller.addOnePage()
}
}
is RoomListDynamicEvents.Reset -> {
controller.resetToOnePage()
}
}
}.launchIn(this)
awaitClose {
result.entriesStream.cancelAndDestroy()
result.destroy()
}
result.entriesStream
}.catch {
Timber.d(it, "entriesFlow() failed")
}.buffer(Channel.UNLIMITED)
fun RoomListServiceInterface.stateFlow(): Flow<RoomListServiceState> =
internal fun RoomListServiceInterface.stateFlow(): Flow<RoomListServiceState> =
mxCallbackFlow {
val listener = object : RoomListServiceStateListener {
override fun onUpdate(state: RoomListServiceState) {
@@ -88,7 +113,7 @@ fun RoomListServiceInterface.stateFlow(): Flow<RoomListServiceState> =
}
}.buffer(Channel.UNLIMITED)
fun RoomListServiceInterface.syncIndicator(): Flow<RoomListServiceSyncIndicator> =
internal fun RoomListServiceInterface.syncIndicator(): Flow<RoomListServiceSyncIndicator> =
mxCallbackFlow {
val listener = object : RoomListServiceSyncIndicatorListener {
override fun onUpdate(syncIndicator: RoomListServiceSyncIndicator) {
@@ -104,7 +129,7 @@ fun RoomListServiceInterface.syncIndicator(): Flow<RoomListServiceSyncIndicator>
}
}.buffer(Channel.UNLIMITED)
fun RoomListServiceInterface.roomOrNull(roomId: String): RoomListItem? {
internal fun RoomListServiceInterface.roomOrNull(roomId: String): RoomListItem? {
return try {
room(roomId)
} catch (exception: Exception) {

View File

@@ -0,0 +1,151 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.impl.roomlist
import io.element.android.libraries.matrix.api.roomlist.DynamicRoomList
import io.element.android.libraries.matrix.api.roomlist.RoomList
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind
import org.matrix.rustcomponents.sdk.RoomListLoadingState
import org.matrix.rustcomponents.sdk.RoomList as InnerRoomList
import org.matrix.rustcomponents.sdk.RoomListService as InnerRoomListService
internal class RoomListFactory(
private val innerRoomListService: InnerRoomListService,
private val coroutineScope: CoroutineScope,
private val dispatcher: CoroutineDispatcher,
private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
) {
/**
* Creates a room list that will load all rooms in a single page.
* It mimics the usage of the old api.
*/
fun createRoomList(
innerProvider: suspend () -> InnerRoomList
): RoomList {
return createRustRoomList(
pageSize = Int.MAX_VALUE,
numberOfPages = 1,
initialFilterKind = RoomListEntriesDynamicFilterKind.All,
innerRoomListProvider = innerProvider
)
}
/**
* Creates a room list that can be used to load more rooms and filter them dynamically.
*/
fun createDynamicRoomList(
pageSize: Int = DynamicRoomList.DEFAULT_PAGE_SIZE,
pagesToLoad: Int = DynamicRoomList.DEFAULT_PAGES_TO_LOAD,
initialFilter: DynamicRoomList.Filter = DynamicRoomList.Filter.None,
innerProvider: suspend () -> InnerRoomList
): DynamicRoomList {
return createRustRoomList(
pageSize = pageSize,
numberOfPages = pagesToLoad,
initialFilterKind = initialFilter.toRustFilter(),
innerRoomListProvider = innerProvider
)
}
private fun createRustRoomList(
pageSize: Int,
numberOfPages: Int,
initialFilterKind: RoomListEntriesDynamicFilterKind,
innerRoomListProvider: suspend () -> InnerRoomList
): RustDynamicRoomList {
val loadingStateFlow: MutableStateFlow<RoomList.LoadingState> = MutableStateFlow(RoomList.LoadingState.NotLoaded)
val summariesFlow = MutableStateFlow<List<RoomSummary>>(emptyList())
val processor = RoomSummaryListProcessor(summariesFlow, innerRoomListService, dispatcher, roomSummaryDetailsFactory)
val dynamicEvents = MutableSharedFlow<RoomListDynamicEvents>()
var innerRoomList: InnerRoomList? = null
coroutineScope.launch(dispatcher) {
innerRoomList = innerRoomListProvider()
innerRoomList?.let { innerRoomList ->
innerRoomList.entriesFlow(
pageSize = pageSize,
numberOfPages = numberOfPages,
initialFilterKind = initialFilterKind,
roomListDynamicEvents = dynamicEvents
).onEach { update ->
processor.postUpdate(update)
}.launchIn(this)
innerRoomList.loadingStateFlow()
.map { it.toLoadingState() }
.onEach {
loadingStateFlow.value = it
}
.launchIn(this)
}
}.invokeOnCompletion {
innerRoomList?.destroy()
}
return RustDynamicRoomList(summariesFlow, loadingStateFlow, dynamicEvents, processor)
}
}
private class RustDynamicRoomList(
override val summaries: MutableStateFlow<List<RoomSummary>>,
override val loadingState: MutableStateFlow<RoomList.LoadingState>,
private val dynamicEvents: MutableSharedFlow<RoomListDynamicEvents>,
private val processor: RoomSummaryListProcessor,
) : DynamicRoomList {
override suspend fun rebuildSummaries() {
processor.rebuildRoomSummaries()
}
override suspend fun updateFilter(filter: DynamicRoomList.Filter) {
val filterEvent = RoomListDynamicEvents.SetFilter(filter.toRustFilter())
dynamicEvents.emit(filterEvent)
}
override suspend fun loadMore() {
dynamicEvents.emit(RoomListDynamicEvents.LoadMore)
}
override suspend fun reset() {
dynamicEvents.emit(RoomListDynamicEvents.Reset)
}
}
private fun RoomListLoadingState.toLoadingState(): RoomList.LoadingState {
return when (this) {
is RoomListLoadingState.Loaded -> RoomList.LoadingState.Loaded(maximumNumberOfRooms?.toInt() ?: 0)
RoomListLoadingState.NotLoaded -> RoomList.LoadingState.NotLoaded
}
}
private fun DynamicRoomList.Filter.toRustFilter(): RoomListEntriesDynamicFilterKind {
return when (this) {
DynamicRoomList.Filter.All -> RoomListEntriesDynamicFilterKind.All
is DynamicRoomList.Filter.NormalizedMatchRoomName -> RoomListEntriesDynamicFilterKind.NormalizedMatchRoomName(this.pattern)
DynamicRoomList.Filter.None -> RoomListEntriesDynamicFilterKind.None
}
}

View File

@@ -16,9 +16,7 @@
package io.element.android.libraries.matrix.impl.roomlist
import io.element.android.libraries.core.coroutine.parallelMap
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.sync.Mutex
@@ -39,21 +37,9 @@ class RoomSummaryListProcessor(
) {
private val roomSummariesByIdentifier = HashMap<String, RoomSummary>()
private val initLatch = CompletableDeferred<Unit>()
private val mutex = Mutex()
suspend fun postEntries(entries: List<RoomListEntry>) {
updateRoomSummaries {
Timber.v("Update rooms from postEntries (with ${entries.size} items) on ${Thread.currentThread()}")
val roomSummaries = entries.parallelMap(::buildSummaryForRoomListEntry)
addAll(roomSummaries)
}
initLatch.complete(Unit)
}
suspend fun postUpdate(updates: List<RoomListEntriesUpdate>) {
// Makes sure to process first entries before update.
initLatch.await()
updateRoomSummaries {
Timber.v("Update rooms from postUpdates (with ${updates.size} items) on ${Thread.currentThread()}")
updates.forEach { update ->
@@ -65,7 +51,7 @@ class RoomSummaryListProcessor(
suspend fun rebuildRoomSummaries() {
updateRoomSummaries {
forEachIndexed { i, summary ->
this[i] = when(summary) {
this[i] = when (summary) {
is RoomSummary.Empty -> summary
is RoomSummary.Filled -> buildAndCacheRoomSummaryForIdentifier(summary.identifier())
}

View File

@@ -18,79 +18,34 @@ package io.element.android.libraries.matrix.impl.roomlist
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.RoomSummary
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
import org.matrix.rustcomponents.sdk.RoomListException
import org.matrix.rustcomponents.sdk.RoomListInput
import org.matrix.rustcomponents.sdk.RoomListLoadingState
import org.matrix.rustcomponents.sdk.RoomListRange
import org.matrix.rustcomponents.sdk.RoomListServiceState
import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicator
import timber.log.Timber
import org.matrix.rustcomponents.sdk.RoomListService as InnerRustRoomListService
class RustRoomListService(
internal class RustRoomListService(
private val innerRoomListService: InnerRustRoomListService,
private val sessionCoroutineScope: CoroutineScope,
private val dispatcher: CoroutineDispatcher,
roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
private val roomListFactory: RoomListFactory,
) : RoomListService {
private val allRooms = MutableStateFlow<List<RoomSummary>>(emptyList())
private val inviteRooms = MutableStateFlow<List<RoomSummary>>(emptyList())
private val allRoomsLoadingState: MutableStateFlow<RoomList.LoadingState> = MutableStateFlow(RoomList.LoadingState.NotLoaded)
private val allRoomsListProcessor = RoomSummaryListProcessor(allRooms, innerRoomListService, dispatcher, roomSummaryDetailsFactory)
private val invitesLoadingState: MutableStateFlow<RoomList.LoadingState> = MutableStateFlow(RoomList.LoadingState.NotLoaded)
private val inviteRoomsListProcessor = RoomSummaryListProcessor(inviteRooms, innerRoomListService, dispatcher, roomSummaryDetailsFactory)
init {
sessionCoroutineScope.launch(dispatcher) {
val allRooms = innerRoomListService.allRooms()
allRooms
.observeEntriesWithProcessor(allRoomsListProcessor)
.launchIn(this)
allRooms
.observeLoadingState(allRoomsLoadingState)
.launchIn(this)
launch {
// Wait until running, as invites is only available after that
innerRoomListService.stateFlow().first {
it == RoomListServiceState.RUNNING
}
val invites = innerRoomListService.invites()
invites
.observeEntriesWithProcessor(inviteRoomsListProcessor)
.launchIn(this)
invites
.observeLoadingState(invitesLoadingState)
.launchIn(this)
}
}
override val allRooms: RoomList = roomListFactory.createRoomList {
innerRoomListService.allRooms()
}
override fun allRooms(): RoomList {
return RustRoomList(allRooms, allRoomsLoadingState)
}
override fun invites(): RoomList {
return RustRoomList(inviteRooms, invitesLoadingState)
override val invites: RoomList = roomListFactory.createRoomList {
innerRoomListService.invites()
}
override fun updateAllRoomsVisibleRange(range: IntRange) {
@@ -107,12 +62,6 @@ class RustRoomListService(
}
}
override fun rebuildRoomSummaries() {
sessionCoroutineScope.launch {
allRoomsListProcessor.rebuildRoomSummaries()
}
}
override val syncIndicator: StateFlow<RoomListService.SyncIndicator> =
innerRoomListService.syncIndicator()
.map { it.toSyncIndicator() }
@@ -132,13 +81,6 @@ class RustRoomListService(
.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, RoomListService.State.Idle)
}
private fun RoomListLoadingState.toLoadingState(): RoomList.LoadingState {
return when (this) {
is RoomListLoadingState.Loaded -> RoomList.LoadingState.Loaded(maximumNumberOfRooms?.toInt() ?: 0)
RoomListLoadingState.NotLoaded -> RoomList.LoadingState.NotLoaded
}
}
private fun RoomListServiceState.toRoomListState(): RoomListService.State {
return when (this) {
RoomListServiceState.INITIAL,
@@ -156,20 +98,3 @@ private fun RoomListServiceSyncIndicator.toSyncIndicator(): RoomListService.Sync
RoomListServiceSyncIndicator.HIDE -> RoomListService.SyncIndicator.Hide
}
}
private fun org.matrix.rustcomponents.sdk.RoomList.observeEntriesWithProcessor(processor: RoomSummaryListProcessor): Flow<List<RoomListEntriesUpdate>> {
return entriesFlow { roomListEntries ->
processor.postEntries(roomListEntries)
}.onEach { update ->
processor.postUpdate(update)
}
}
private fun org.matrix.rustcomponents.sdk.RoomList.observeLoadingState(stateFlow: MutableStateFlow<RoomList.LoadingState>): Flow<RoomList.LoadingState> {
return loadingStateFlow()
.map { it.toLoadingState() }
.onEach {
stateFlow.value = it
}
}

View File

@@ -22,12 +22,10 @@ import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withTimeout
import org.junit.Test
import org.matrix.rustcomponents.sdk.RoomList
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
@@ -38,7 +36,6 @@ import org.matrix.rustcomponents.sdk.RoomListServiceInterface
import org.matrix.rustcomponents.sdk.RoomListServiceStateListener
import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicatorListener
import org.matrix.rustcomponents.sdk.TaskHandle
import kotlin.time.Duration.Companion.milliseconds
// NOTE: this class is using a fake implementation of a Rust SDK interface which returns actual Rust objects with pointers.
// Since we don't access the data in those objects, this is fine for our tests, but that's as far as we can test this class.
@@ -46,38 +43,11 @@ class RoomSummaryListProcessorTests {
private val summaries = MutableStateFlow<List<RoomSummary>>(emptyList())
@Test
fun `postUpdates can't start until postEntries is done`() = runTest {
val processor = createProcessor()
val update = listOf(RoomListEntriesUpdate.Reset(emptyList()))
val timeoutError = runCatching {
withTimeout(10.milliseconds) { processor.postUpdate(update) }
}.exceptionOrNull()
assertThat(timeoutError).isInstanceOf(CancellationException::class.java)
processor.postEntries(listOf(RoomListEntry.Empty))
processor.postUpdate(update)
}
@Test
fun `postEntries adds all new entries with no diffing`() = runTest {
summaries.value = listOf(aRoomSummaryFilled())
val processor = createProcessor()
processor.postEntries(listOf(RoomListEntry.Empty, RoomListEntry.Empty, RoomListEntry.Empty))
assertThat(summaries.value.count()).isEqualTo(4)
}
@Test
fun `Append adds new entries at the end of the list`() = runTest {
summaries.value = listOf(aRoomSummaryFilled())
val processor = createProcessor()
// Start processing updates
processor.postEntries(listOf())
// Process actual update
processor.postUpdate(listOf(RoomListEntriesUpdate.Append(listOf(RoomListEntry.Empty, RoomListEntry.Empty, RoomListEntry.Empty))))
assertThat(summaries.value.count()).isEqualTo(4)
@@ -88,10 +58,6 @@ class RoomSummaryListProcessorTests {
fun `PushBack adds a new entry at the end of the list`() = runTest {
summaries.value = listOf(aRoomSummaryFilled())
val processor = createProcessor()
// Start processing updates
processor.postEntries(listOf())
// Process actual update
processor.postUpdate(listOf(RoomListEntriesUpdate.PushBack(RoomListEntry.Empty)))
assertThat(summaries.value.count()).isEqualTo(2)
@@ -102,10 +68,6 @@ class RoomSummaryListProcessorTests {
fun `PushFront inserts a new entry at the start of the list`() = runTest {
summaries.value = listOf(aRoomSummaryFilled())
val processor = createProcessor()
// Start processing updates
processor.postEntries(listOf())
// Process actual update
processor.postUpdate(listOf(RoomListEntriesUpdate.PushFront(RoomListEntry.Empty)))
assertThat(summaries.value.count()).isEqualTo(2)
@@ -118,9 +80,6 @@ class RoomSummaryListProcessorTests {
val processor = createProcessor()
val index = 0
// Start processing updates
processor.postEntries(listOf())
// Process actual update
processor.postUpdate(listOf(RoomListEntriesUpdate.Set(index.toUInt(), RoomListEntry.Empty)))
assertThat(summaries.value.count()).isEqualTo(1)
@@ -133,9 +92,6 @@ class RoomSummaryListProcessorTests {
val processor = createProcessor()
val index = 0
// Start processing updates
processor.postEntries(listOf())
// Process actual update
processor.postUpdate(listOf(RoomListEntriesUpdate.Insert(index.toUInt(), RoomListEntry.Empty)))
assertThat(summaries.value.count()).isEqualTo(2)
@@ -148,9 +104,6 @@ class RoomSummaryListProcessorTests {
val processor = createProcessor()
val index = 0
// Start processing updates
processor.postEntries(listOf())
// Process actual update
processor.postUpdate(listOf(RoomListEntriesUpdate.Remove(index.toUInt())))
assertThat(summaries.value.count()).isEqualTo(1)
@@ -163,9 +116,6 @@ class RoomSummaryListProcessorTests {
val processor = createProcessor()
val index = 0
// Start processing updates
processor.postEntries(listOf())
// Process actual update
processor.postUpdate(listOf(RoomListEntriesUpdate.PopBack))
assertThat(summaries.value.count()).isEqualTo(1)
@@ -178,9 +128,6 @@ class RoomSummaryListProcessorTests {
val processor = createProcessor()
val index = 0
// Start processing updates
processor.postEntries(listOf())
// Process actual update
processor.postUpdate(listOf(RoomListEntriesUpdate.PopFront))
assertThat(summaries.value.count()).isEqualTo(1)
@@ -192,9 +139,6 @@ class RoomSummaryListProcessorTests {
summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2))
val processor = createProcessor()
// Start processing updates
processor.postEntries(listOf())
// Process actual update
processor.postUpdate(listOf(RoomListEntriesUpdate.Clear))
assertThat(summaries.value).isEmpty()
@@ -206,9 +150,6 @@ class RoomSummaryListProcessorTests {
val processor = createProcessor()
val index = 0
// Start processing updates
processor.postEntries(listOf())
// Process actual update
processor.postUpdate(listOf(RoomListEntriesUpdate.Truncate(1u)))
assertThat(summaries.value.count()).isEqualTo(1)

View File

@@ -54,28 +54,20 @@ class FakeRoomListService : RoomListService {
var latestSlidingSyncRange: IntRange? = null
private set
override val allRooms: RoomList = SimplePagedRoomList(
allRoomSummariesFlow,
allRoomsLoadingStateFlow,
)
override val invites: RoomList = SimplePagedRoomList(
inviteRoomSummariesFlow,
inviteRoomsLoadingStateFlow,
)
override fun updateAllRoomsVisibleRange(range: IntRange) {
latestSlidingSyncRange = range
}
override fun rebuildRoomSummaries() {
}
override fun allRooms(): RoomList {
return SimpleRoomList(
summaries = allRoomSummariesFlow,
loadingState = allRoomsLoadingStateFlow
)
}
override fun invites(): RoomList {
return SimpleRoomList(
summaries = inviteRoomSummariesFlow,
loadingState = inviteRoomsLoadingStateFlow
)
}
override val state: StateFlow<RoomListService.State> = roomListStateFlow
override val syncIndicator: StateFlow<RoomListService.SyncIndicator> = syncIndicatorStateFlow

View File

@@ -16,11 +16,29 @@
package io.element.android.libraries.matrix.test.roomlist
import io.element.android.libraries.matrix.api.roomlist.DynamicRoomList
import io.element.android.libraries.matrix.api.roomlist.RoomList
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.coroutines.flow.StateFlow
data class SimpleRoomList(
data class SimplePagedRoomList(
override val summaries: StateFlow<List<RoomSummary>>,
override val loadingState: StateFlow<RoomList.LoadingState>
) : RoomList
) : DynamicRoomList {
override suspend fun loadMore() {
//No-op
}
override suspend fun reset() {
//No-op
}
override suspend fun updateFilter(filter: DynamicRoomList.Filter) {
//No-op
}
override suspend fun rebuildSummaries() {
//No-op
}
}