From 2177a62ff12c357f40dcab35ec661afc75859c54 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 8 Oct 2026 17:42:29 +0200 Subject: [PATCH] Apply the room list filter requested before the controller is created The dynamic entries controller of a room list is created asynchronously, once the entries are observed. Filters set before that were silently dropped, so for instance the search query was not applied to the rooms of a newly selected account in the room select screen. RustDynamicRoomList now owns the controller, remembers the last requested filter, and applies it when the controller is created if it differs from the initial one. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../impl/roomlist/RoomListExtensions.kt | 2 +- .../matrix/impl/roomlist/RoomListFactory.kt | 19 +++--- .../impl/roomlist/RustDynamicRoomList.kt | 43 +++++++++--- ...FakeFfiRoomListDynamicEntriesController.kt | 25 +++++++ .../impl/roomlist/RustDynamicRoomListTest.kt | 67 +++++++++++++++++++ 5 files changed, 134 insertions(+), 22 deletions(-) create mode 100644 libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoomListDynamicEntriesController.kt create mode 100644 libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustDynamicRoomListTest.kt 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 bf57e4295b..301fb44bea 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 @@ -57,7 +57,7 @@ fun RoomListInterface.loadingStateFlow(): Flow = internal fun RoomListInterface.entriesFlow( pageSize: Int, initialFilterKind: RoomListEntriesDynamicFilterKind, - onControllerCreated: (RoomListDynamicEntriesController) -> Unit, + onControllerCreated: suspend (RoomListDynamicEntriesController) -> Unit, ): Flow> = callbackFlow { val listener = object : RoomListEntriesListener { 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 1d90b07104..c12ddbede8 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,7 +23,6 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import org.matrix.rustcomponents.sdk.RoomListDynamicEntriesController import org.matrix.rustcomponents.sdk.RoomListLoadingState import org.matrix.rustcomponents.sdk.RoomListService import kotlin.coroutines.CoroutineContext @@ -49,7 +48,13 @@ internal class RoomListFactory( val summariesFlow = MutableSharedFlow>(replay = 1, extraBufferCapacity = 1) val processor = RoomSummaryListProcessor(summariesFlow, innerRoomListService, coroutineContext, roomSummaryFactory, analyticsService) var innerRoomList: InnerRoomList? = null - var dynamicController: RoomListDynamicEntriesController? = null + val dynamicRoomList = RustDynamicRoomList( + summaries = summariesFlow, + loadingState = loadingStateFlow, + processor = processor, + pageSize = pageSize, + initialFilter = initialFilter, + ) val firstRoomsTransaction = analyticsService.startTransaction("Load first set of rooms", "innerRoomList.entriesFlow") @@ -60,7 +65,7 @@ internal class RoomListFactory( pageSize = pageSize, initialFilterKind = RoomListFilterMapper.toRustFilter(initialFilter), onControllerCreated = { controller -> - dynamicController = controller + dynamicRoomList.onControllerCreated(controller, initialFilter) } ).onEach { update -> if (!firstRoomsTransaction.isFinished()) { @@ -80,13 +85,7 @@ internal class RoomListFactory( }.invokeOnCompletion { innerRoomList?.destroy() } - return RustDynamicRoomList( - summaries = summariesFlow, - loadingState = loadingStateFlow, - processor = processor, - pageSize = pageSize, - dynamicController = { dynamicController } - ) + return dynamicRoomList } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustDynamicRoomList.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustDynamicRoomList.kt index 0d7b2394b3..5161b3d13a 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustDynamicRoomList.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustDynamicRoomList.kt @@ -24,37 +24,58 @@ internal class RustDynamicRoomList( override val loadingState: MutableStateFlow, private val processor: RoomSummaryListProcessor, override val pageSize: Int, - private val dynamicController: () -> RoomListDynamicEntriesController?, + initialFilter: RoomListFilter, private val addPagesCount: Int = DEFAULT_ADD_PAGES_COUNT ) : DynamicRoomList { private val mutex = Mutex() + // The controller is created asynchronously, once the entries of the room list are observed + private var dynamicController: RoomListDynamicEntriesController? = null + + // The last requested filter, so that it can be applied when the controller is created + private var currentFilter: RoomListFilter = initialFilter + + /** + * To be called when the controller is created. The controller already has the initial filter set, so apply the filter + * only if it has been updated in the meantime. + */ + suspend fun onControllerCreated(controller: RoomListDynamicEntriesController, initialFilter: RoomListFilter) { + mutex.withLock { + dynamicController = controller + if (currentFilter != initialFilter) { + controller.applyFilter(currentFilter) + } + } + } + override suspend fun rebuildSummaries() { processor.rebuildRoomSummaries() } override suspend fun updateFilter(filter: RoomListFilter) { mutex.withLock { - dynamicController()?.let { controller -> - // Reset pagination when filter changes - controller.resetToOnePage() - val rustFilter = RoomListFilterMapper.toRustFilter(filter) - controller.setFilter(rustFilter) - // Then preload some pages - controller.addPages(addPagesCount) - } + currentFilter = filter + dynamicController?.applyFilter(filter) } } + private fun RoomListDynamicEntriesController.applyFilter(filter: RoomListFilter) { + // Reset pagination when filter changes + resetToOnePage() + setFilter(RoomListFilterMapper.toRustFilter(filter)) + // Then preload some pages + addPages(addPagesCount) + } + override suspend fun loadMore() { mutex.withLock { - dynamicController()?.addPages(addPagesCount) + dynamicController?.addPages(addPagesCount) } } override suspend fun reset() { mutex.withLock { - dynamicController()?.resetToOnePage() + dynamicController?.resetToOnePage() } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoomListDynamicEntriesController.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoomListDynamicEntriesController.kt new file mode 100644 index 0000000000..9a80841069 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoomListDynamicEntriesController.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.fixtures.fakes + +import org.matrix.rustcomponents.sdk.NoHandle +import org.matrix.rustcomponents.sdk.RoomListDynamicEntriesController +import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind + +class FakeFfiRoomListDynamicEntriesController : RoomListDynamicEntriesController(NoHandle) { + val filters = mutableListOf() + + override fun setFilter(kind: RoomListEntriesDynamicFilterKind): Boolean { + filters.add(kind) + return true + } + + override fun resetToOnePage() = Unit + + override fun addOnePage() = Unit +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustDynamicRoomListTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustDynamicRoomListTest.kt new file mode 100644 index 0000000000..fc2686f882 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustDynamicRoomListTest.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.roomlist + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.roomlist.RoomList +import io.element.android.libraries.matrix.api.roomlist.RoomListFilter +import io.element.android.libraries.matrix.api.roomlist.RoomSummary +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoomListDynamicEntriesController +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoomListService +import io.element.android.services.analytics.test.FakeAnalyticsService +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class RustDynamicRoomListTest { + @Test + fun `updateFilter before the controller is created applies the filter when it is created`() = runTest { + val sut = createRustDynamicRoomList() + sut.updateFilter(RoomListFilter.NormalizedMatchRoomName("query")) + val controller = FakeFfiRoomListDynamicEntriesController() + sut.onControllerCreated(controller, initialFilter = RoomListFilter.all()) + assertThat(controller.filters).containsExactly(RoomListFilterMapper.toRustFilter(RoomListFilter.NormalizedMatchRoomName("query"))) + } + + @Test + fun `when the filter is not updated, the controller filter is not changed when it is created`() = runTest { + val sut = createRustDynamicRoomList() + val controller = FakeFfiRoomListDynamicEntriesController() + sut.onControllerCreated(controller, initialFilter = RoomListFilter.all()) + assertThat(controller.filters).isEmpty() + } + + @Test + fun `updateFilter after the controller is created applies the filter`() = runTest { + val sut = createRustDynamicRoomList() + val controller = FakeFfiRoomListDynamicEntriesController() + sut.onControllerCreated(controller, initialFilter = RoomListFilter.all()) + sut.updateFilter(RoomListFilter.NormalizedMatchRoomName("query")) + assertThat(controller.filters).containsExactly(RoomListFilterMapper.toRustFilter(RoomListFilter.NormalizedMatchRoomName("query"))) + } + + private fun TestScope.createRustDynamicRoomList(): RustDynamicRoomList { + val summaries = MutableSharedFlow>(replay = 1) + return RustDynamicRoomList( + summaries = summaries, + loadingState = MutableStateFlow(RoomList.LoadingState.NotLoaded), + processor = RoomSummaryListProcessor( + summaries, + FakeFfiRoomListService(), + coroutineContext = StandardTestDispatcher(testScheduler), + roomSummaryFactory = RoomSummaryFactory(), + analyticsService = FakeAnalyticsService(), + ), + pageSize = 10, + initialFilter = RoomListFilter.all(), + ) + } +}