From d04b26365dac729c8334db243e4475ca1b969568 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 22 Nov 2023 18:06:19 +0100 Subject: [PATCH] RoomList : rework a bit the api and make usage of entriesWithDynamicAdapters --- .../invitelist/impl/InviteListPresenter.kt | 2 +- .../impl/forward/ForwardMessagesPresenter.kt | 2 +- ...EditDefaultNotificationSettingPresenter.kt | 2 +- .../DefaultInviteStateDataSource.kt | 2 +- .../impl/datasource/RoomListDataSource.kt | 4 +- .../matrix/api/roomlist/DynamicRoomList.kt | 63 ++++++++ .../libraries/matrix/api/roomlist/RoomList.kt | 7 + .../matrix/api/roomlist/RoomListService.kt | 9 +- .../libraries/matrix/impl/RustMatrixClient.kt | 11 +- ...stRoomList.kt => RoomListDynamicEvents.kt} | 16 +- .../impl/roomlist/RoomListExtensions.kt | 49 ++++-- .../matrix/impl/roomlist/RoomListFactory.kt | 151 ++++++++++++++++++ .../impl/roomlist/RoomSummaryListProcessor.kt | 16 +- .../impl/roomlist/RustRoomListService.kt | 87 +--------- .../roomlist/RoomSummaryListProcessorTests.kt | 59 ------- .../test/roomlist/FakeRoomListService.kt | 28 ++-- ...mpleRoomList.kt => SimplePagedRoomList.kt} | 22 ++- 17 files changed, 317 insertions(+), 213 deletions(-) create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/DynamicRoomList.kt rename libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/{RustRoomList.kt => RoomListDynamicEvents.kt} (60%) create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactory.kt rename libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/{SimpleRoomList.kt => SimplePagedRoomList.kt} (70%) diff --git a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListPresenter.kt b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListPresenter.kt index 648eb5094f..8bc752d629 100644 --- a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListPresenter.kt +++ b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListPresenter.kt @@ -57,7 +57,7 @@ class InviteListPresenter @Inject constructor( override fun present(): InviteListState { val invites by client .roomListService - .invites() + .invites .summaries .collectAsState() diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt index 273ed5906d..b7d77ed8cb 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt @@ -65,7 +65,7 @@ class ForwardMessagesPresenter @AssistedInject constructor( var results: SearchBarResultState> by remember { mutableStateOf(SearchBarResultState.NotSearching()) } val forwardingActionState: MutableState>> = 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() diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingPresenter.kt index 79201e27d3..5d8b149676 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingPresenter.kt @@ -105,7 +105,7 @@ class EditDefaultNotificationSettingPresenter @AssistedInject constructor( } private fun CoroutineScope.observeRoomSummaries(roomsWithUserDefinedMode: MutableState>) { - roomListService.allRooms() + roomListService.allRooms .summaries .onEach { updateRoomsWithUserDefinedMode(it, roomsWithUserDefinedMode) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/DefaultInviteStateDataSource.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/DefaultInviteStateDataSource.kt index e05efd3ddd..dc89d163c3 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/DefaultInviteStateDataSource.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/DefaultInviteStateDataSource.kt @@ -45,7 +45,7 @@ class DefaultInviteStateDataSource @Inject constructor( override fun inviteState(): InvitesState { val invites by client .roomListService - .invites() + .invites .summaries .collectAsState() diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSource.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSource.kt index b8af0a60cc..e337fe5ad3 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSource.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSource.kt @@ -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) } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/DynamicRoomList.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/DynamicRoomList.kt new file mode 100644 index 0000000000..8248aaea90 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/DynamicRoomList.kt @@ -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) +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomList.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomList.kt index 3d429766b0..8722e2c5bd 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomList.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomList.kt @@ -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 + + /** + * 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) { diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt index 3bd445a282..9897c2cff5 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt @@ -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. */ diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 8429968af3..c277a01791 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -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) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomList.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListDynamicEvents.kt similarity index 60% rename from libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomList.kt rename to libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListDynamicEvents.kt index 481b38dd9b..97d21f83f2 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomList.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListDynamicEvents.kt @@ -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>, - override val loadingState: StateFlow -) : RoomList +internal sealed interface RoomListDynamicEvents { + data object Reset : RoomListDynamicEvents + data object LoadMore : RoomListDynamicEvents + data class SetFilter(val filter: RoomListEntriesDynamicFilterKind) : RoomListDynamicEvents +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt index b95eb95333..5afd7c4174 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt @@ -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 = Timber.d(it, "loadingStateFlow() failed") }.buffer(Channel.UNLIMITED) -fun RoomListInterface.entriesFlow(onInitialList: suspend (List) -> Unit): Flow> = - mxCallbackFlow { +internal fun RoomListInterface.entriesFlow( + pageSize: Int, + numberOfPages: Int, + roomListDynamicEvents: Flow, + initialFilterKind: RoomListEntriesDynamicFilterKind +): Flow> = + callbackFlow { val listener = object : RoomListEntriesListener { override fun onUpdate(roomEntriesUpdate: List) { 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 = +internal fun RoomListServiceInterface.stateFlow(): Flow = mxCallbackFlow { val listener = object : RoomListServiceStateListener { override fun onUpdate(state: RoomListServiceState) { @@ -88,7 +113,7 @@ fun RoomListServiceInterface.stateFlow(): Flow = } }.buffer(Channel.UNLIMITED) -fun RoomListServiceInterface.syncIndicator(): Flow = +internal fun RoomListServiceInterface.syncIndicator(): Flow = mxCallbackFlow { val listener = object : RoomListServiceSyncIndicatorListener { override fun onUpdate(syncIndicator: RoomListServiceSyncIndicator) { @@ -104,7 +129,7 @@ fun RoomListServiceInterface.syncIndicator(): Flow } }.buffer(Channel.UNLIMITED) -fun RoomListServiceInterface.roomOrNull(roomId: String): RoomListItem? { +internal fun RoomListServiceInterface.roomOrNull(roomId: String): RoomListItem? { return try { room(roomId) } catch (exception: Exception) { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactory.kt new file mode 100644 index 0000000000..efe65deeb0 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactory.kt @@ -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 = MutableStateFlow(RoomList.LoadingState.NotLoaded) + val summariesFlow = MutableStateFlow>(emptyList()) + val processor = RoomSummaryListProcessor(summariesFlow, innerRoomListService, dispatcher, roomSummaryDetailsFactory) + val dynamicEvents = MutableSharedFlow() + + 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>, + override val loadingState: MutableStateFlow, + private val dynamicEvents: MutableSharedFlow, + 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 + } +} + diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt index d0e3d1c8cf..f0dd3a4176 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt @@ -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() - private val initLatch = CompletableDeferred() private val mutex = Mutex() - suspend fun postEntries(entries: List) { - 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) { - // 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()) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt index 47b4792ca3..171094c5f3 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt @@ -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>(emptyList()) - private val inviteRooms = MutableStateFlow>(emptyList()) - - private val allRoomsLoadingState: MutableStateFlow = MutableStateFlow(RoomList.LoadingState.NotLoaded) - private val allRoomsListProcessor = RoomSummaryListProcessor(allRooms, innerRoomListService, dispatcher, roomSummaryDetailsFactory) - private val invitesLoadingState: MutableStateFlow = 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 = 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> { - return entriesFlow { roomListEntries -> - processor.postEntries(roomListEntries) - }.onEach { update -> - processor.postUpdate(update) - } -} - -private fun org.matrix.rustcomponents.sdk.RoomList.observeLoadingState(stateFlow: MutableStateFlow): Flow { - return loadingStateFlow() - .map { it.toLoadingState() } - .onEach { - stateFlow.value = it - } -} - diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessorTests.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessorTests.kt index 5a376ea928..e4cd1c3abd 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessorTests.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessorTests.kt @@ -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>(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) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/FakeRoomListService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/FakeRoomListService.kt index 75a91508d0..ece77d53b5 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/FakeRoomListService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/FakeRoomListService.kt @@ -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 = roomListStateFlow override val syncIndicator: StateFlow = syncIndicatorStateFlow diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/SimpleRoomList.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/SimplePagedRoomList.kt similarity index 70% rename from libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/SimpleRoomList.kt rename to libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/SimplePagedRoomList.kt index 28b04ae318..2555fe937d 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/SimpleRoomList.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/SimplePagedRoomList.kt @@ -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>, override val loadingState: StateFlow -) : 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 + } +}