diff --git a/changelog.d/1920.misc b/changelog.d/1920.misc new file mode 100644 index 0000000000..4c59527599 --- /dev/null +++ b/changelog.d/1920.misc @@ -0,0 +1 @@ +RoomList: introduce incremental loading to improve performances. 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 index 8248aaea90..1f46c2780d 100644 --- 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 @@ -16,6 +16,12 @@ package io.element.android.libraries.matrix.api.roomlist +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + /** * RoomList with dynamic filtering and loading. * This is useful for large lists of rooms. @@ -23,17 +29,17 @@ package io.element.android.libraries.matrix.api.roomlist */ 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 only the left rooms. + */ + data object AllNonLeft : Filter + /** * Filter all rooms. */ @@ -45,6 +51,10 @@ interface DynamicRoomList : RoomList { data class NormalizedMatchRoomName(val pattern: String) : Filter } + val currentFilter: StateFlow + val loadedPages: StateFlow + val pageSize: Int + /** * Load more rooms into the list if possible. */ @@ -61,3 +71,29 @@ interface DynamicRoomList : RoomList { */ suspend fun updateFilter(filter: Filter) } + +/** + * Offers a way to load all the rooms incrementally. + * It will load more room until all are loaded. + * If total number of rooms increase, it will load more pages if needed. + * The number of rooms is independent of the filter. + */ +fun DynamicRoomList.loadAllIncrementally(coroutineScope: CoroutineScope) { + combine( + loadedPages, + loadingState, + ) { loadedPages, loadingState -> + loadedPages to loadingState + } + .onEach { (loadedPages, loadingState) -> + when (loadingState) { + is RoomList.LoadingState.Loaded -> { + if (pageSize * loadedPages < loadingState.numberOfRooms) { + loadMore() + } + } + RoomList.LoadingState.NotLoaded -> Unit + } + } + .launchIn(coroutineScope) +} 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 ca38128e5d..5682a43389 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,10 +41,10 @@ interface RoomListService { } /** - * returns a [RoomList] object of all rooms we want to display. + * returns a [DynamicRoomList] object of all rooms we want to display. * This will exclude some rooms like the invites, or spaces. */ - val allRooms: RoomList + val allRooms: DynamicRoomList /** * returns a [RoomList] object of all invites. 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 5afd7c4174..79ea6b17e0 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 @@ -65,7 +65,6 @@ fun RoomListInterface.loadingStateFlow(): Flow = internal fun RoomListInterface.entriesFlow( pageSize: Int, - numberOfPages: Int, roomListDynamicEvents: Flow, initialFilterKind: RoomListEntriesDynamicFilterKind ): Flow> = @@ -84,9 +83,7 @@ internal fun RoomListInterface.entriesFlow( controller.setFilter(controllerEvents.filter) } is RoomListDynamicEvents.LoadMore -> { - repeat(numberOfPages) { - controller.addOnePage() - } + controller.addOnePage() } is RoomListDynamicEvents.Reset -> { controller.resetToOnePage() 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 index b1ed372633..17bff4f5b7 100644 --- 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 @@ -23,6 +23,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -39,57 +40,28 @@ internal class RoomListFactory( 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.AllNonLeft, - 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, + fun createRoomList( + pageSize: Int, + initialFilter: DynamicRoomList.Filter = DynamicRoomList.Filter.All, 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() - + // Makes sure we don't miss any events + val dynamicEvents = MutableSharedFlow(replay = 100) + val currentFilter = MutableStateFlow(initialFilter) + val loadedPages = MutableStateFlow(1) var innerRoomList: InnerRoomList? = null coroutineScope.launch(dispatcher) { - innerRoomList = innerRoomListProvider() + innerRoomList = innerProvider() innerRoomList?.let { innerRoomList -> innerRoomList.entriesFlow( pageSize = pageSize, - numberOfPages = numberOfPages, - initialFilterKind = initialFilterKind, + initialFilterKind = initialFilter.toRustFilter(), roomListDynamicEvents = dynamicEvents ).onEach { update -> processor.postUpdate(update) @@ -105,15 +77,26 @@ internal class RoomListFactory( }.invokeOnCompletion { innerRoomList?.destroy() } - return RustDynamicRoomList(summariesFlow, loadingStateFlow, dynamicEvents, processor) + return RustDynamicRoomList( + summaries = summariesFlow, + loadingState = loadingStateFlow, + currentFilter = currentFilter, + loadedPages = loadedPages, + dynamicEvents = dynamicEvents, + processor = processor, + pageSize = pageSize, + ) } } private class RustDynamicRoomList( override val summaries: MutableStateFlow>, override val loadingState: MutableStateFlow, + override val currentFilter: MutableStateFlow, + override val loadedPages: MutableStateFlow, private val dynamicEvents: MutableSharedFlow, private val processor: RoomSummaryListProcessor, + override val pageSize: Int, ) : DynamicRoomList { override suspend fun rebuildSummaries() { @@ -121,16 +104,19 @@ private class RustDynamicRoomList( } override suspend fun updateFilter(filter: DynamicRoomList.Filter) { + currentFilter.emit(filter) val filterEvent = RoomListDynamicEvents.SetFilter(filter.toRustFilter()) dynamicEvents.emit(filterEvent) } override suspend fun loadMore() { dynamicEvents.emit(RoomListDynamicEvents.LoadMore) + loadedPages.getAndUpdate { it + 1 } } override suspend fun reset() { dynamicEvents.emit(RoomListDynamicEvents.Reset) + loadedPages.emit(1) } } @@ -146,6 +132,7 @@ private fun DynamicRoomList.Filter.toRustFilter(): RoomListEntriesDynamicFilterK DynamicRoomList.Filter.All -> RoomListEntriesDynamicFilterKind.All is DynamicRoomList.Filter.NormalizedMatchRoomName -> RoomListEntriesDynamicFilterKind.NormalizedMatchRoomName(this.pattern) DynamicRoomList.Filter.None -> RoomListEntriesDynamicFilterKind.None + DynamicRoomList.Filter.AllNonLeft -> RoomListEntriesDynamicFilterKind.AllNonLeft } } 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 6ae677831a..3dce8a0fc4 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 @@ -16,8 +16,10 @@ 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.RoomListService +import io.element.android.libraries.matrix.api.roomlist.loadAllIncrementally import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -34,20 +36,31 @@ import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicator import timber.log.Timber import org.matrix.rustcomponents.sdk.RoomListService as InnerRustRoomListService +private const val DEFAULT_PAGE_SIZE = 20 + internal class RustRoomListService( private val innerRoomListService: InnerRustRoomListService, private val sessionCoroutineScope: CoroutineScope, roomListFactory: RoomListFactory, ) : RoomListService { - override val allRooms: RoomList = roomListFactory.createRoomList { + override val allRooms: DynamicRoomList = roomListFactory.createRoomList( + pageSize = DEFAULT_PAGE_SIZE, + initialFilter = DynamicRoomList.Filter.AllNonLeft, + ) { innerRoomListService.allRooms() } - override val invites: RoomList = roomListFactory.createRoomList { + override val invites: RoomList = roomListFactory.createRoomList( + pageSize = Int.MAX_VALUE, + ) { innerRoomListService.invites() } + init { + allRooms.loadAllIncrementally(sessionCoroutineScope) + } + override fun updateAllRoomsVisibleRange(range: IntRange) { Timber.v("setVisibleRange=$range") sessionCoroutineScope.launch { 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 ece77d53b5..5c7d0983cd 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 @@ -16,6 +16,7 @@ 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.RoomListService import io.element.android.libraries.matrix.api.roomlist.RoomSummary @@ -54,14 +55,16 @@ class FakeRoomListService : RoomListService { var latestSlidingSyncRange: IntRange? = null private set - override val allRooms: RoomList = SimplePagedRoomList( + override val allRooms: DynamicRoomList = SimplePagedRoomList( allRoomSummariesFlow, allRoomsLoadingStateFlow, + MutableStateFlow(DynamicRoomList.Filter.None) ) override val invites: RoomList = SimplePagedRoomList( inviteRoomSummariesFlow, inviteRoomsLoadingStateFlow, + MutableStateFlow(DynamicRoomList.Filter.None) ) override fun updateAllRoomsVisibleRange(range: IntRange) { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/SimplePagedRoomList.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/SimplePagedRoomList.kt index 2555fe937d..e94002bd1d 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/SimplePagedRoomList.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/SimplePagedRoomList.kt @@ -19,23 +19,30 @@ 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.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.getAndUpdate data class SimplePagedRoomList( override val summaries: StateFlow>, - override val loadingState: StateFlow + override val loadingState: StateFlow, + override val currentFilter: MutableStateFlow ) : DynamicRoomList { + override val pageSize: Int = Int.MAX_VALUE + override val loadedPages = MutableStateFlow(1) + override suspend fun loadMore() { //No-op + loadedPages.getAndUpdate { it + 1 } } override suspend fun reset() { - //No-op + loadedPages.emit(1) } override suspend fun updateFilter(filter: DynamicRoomList.Filter) { - //No-op + currentFilter.emit(filter) } override suspend fun rebuildSummaries() {