From 85a0ef3677857f8891b153102dd1c5669e1404df Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 16 Feb 2024 19:22:06 +0100 Subject: [PATCH] RoomList : rework how search is done to prepare for later filtering --- .../features/roomlist/impl/RoomListEvents.kt | 1 - .../roomlist/impl/RoomListPresenter.kt | 19 +-- .../features/roomlist/impl/RoomListState.kt | 5 +- .../roomlist/impl/RoomListStateProvider.kt | 8 +- .../features/roomlist/impl/RoomListView.kt | 9 +- .../impl/components/RoomListTopBar.kt | 11 -- .../impl/datasource/RoomListDataSource.kt | 26 --- .../impl/search/RoomListSearchEvents.kt | 23 +++ .../impl/search/RoomListSearchPresenter.kt | 120 ++++++++++++++ .../impl/search/RoomListSearchState.kt | 27 ++++ .../search/RoomListSearchStateProvider.kt | 46 ++++++ ...rchResultView.kt => RoomListSearchView.kt} | 77 +++------ .../roomlist/impl/RoomListPresenterTests.kt | 71 +------- .../search/RoomListSearchPresenterTests.kt | 151 ++++++++++++++++++ .../libraries/matrix/api/roomlist/RoomList.kt | 5 + .../matrix/api/roomlist/RoomListFilter.kt | 30 ++++ .../matrix/api/roomlist/RoomListService.kt | 15 ++ .../libraries/matrix/impl/RustMatrixClient.kt | 4 +- .../matrix/impl/roomlist/RoomListFactory.kt | 14 +- .../impl/roomlist/RoomSummaryListProcessor.kt | 6 +- .../impl/roomlist/RustRoomListService.kt | 20 ++- .../roomlist/RoomSummaryListProcessorTests.kt | 2 +- .../matrix/test/room/RoomSummaryFixture.kt | 4 + .../test/roomlist/FakeRoomListService.kt | 12 +- .../test/roomlist/SimplePagedRoomList.kt | 2 +- 25 files changed, 512 insertions(+), 196 deletions(-) create mode 100644 features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchEvents.kt create mode 100644 features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenter.kt create mode 100644 features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchState.kt create mode 100644 features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchStateProvider.kt rename features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/{RoomListSearchResultView.kt => RoomListSearchView.kt} (73%) create mode 100644 features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenterTests.kt diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt index c808ae689b..cad5dd3311 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt @@ -20,7 +20,6 @@ import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.libraries.matrix.api.core.RoomId sealed interface RoomListEvents { - data class UpdateFilter(val newFilter: String) : RoomListEvents data class UpdateVisibleRange(val range: IntRange) : RoomListEvents data object DismissRequestVerificationPrompt : RoomListEvents data object DismissRecoveryKeyPrompt : RoomListEvents diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt index 68ef53703a..7f53f90e92 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt @@ -37,6 +37,8 @@ import io.element.android.features.preferences.api.store.SessionPreferencesStore import io.element.android.features.roomlist.impl.datasource.InviteStateDataSource import io.element.android.features.roomlist.impl.datasource.RoomListDataSource import io.element.android.features.roomlist.impl.migration.MigrationScreenPresenter +import io.element.android.features.roomlist.impl.search.RoomListSearchEvents +import io.element.android.features.roomlist.impl.search.RoomListSearchPresenter import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher @@ -76,6 +78,7 @@ class RoomListPresenter @Inject constructor( private val encryptionService: EncryptionService, private val featureFlagService: FeatureFlagService, private val indicatorService: IndicatorService, + private val searchPresenter: RoomListSearchPresenter, private val migrationScreenPresenter: MigrationScreenPresenter, private val sessionPreferencesStore: SessionPreferencesStore, ) : Presenter { @@ -89,9 +92,8 @@ class RoomListPresenter @Inject constructor( val roomList by produceState(initialValue = AsyncData.Loading()) { roomListDataSource.allRooms.collect { value = AsyncData.Success(it) } } - val filteredRoomList by roomListDataSource.filteredRooms.collectAsState() - val filter by roomListDataSource.filter.collectAsState() val networkConnectionStatus by networkMonitor.connectivity.collectAsState() + val searchState = searchPresenter.present() LaunchedEffect(Unit) { roomListDataSource.launchIn(this) @@ -122,21 +124,14 @@ class RoomListPresenter @Inject constructor( // Avatar indicator val showAvatarIndicator by indicatorService.showRoomListTopBarIndicator() - var displaySearchResults by rememberSaveable { mutableStateOf(false) } val contextMenu = remember { mutableStateOf(RoomListState.ContextMenu.Hidden) } fun handleEvents(event: RoomListEvents) { when (event) { - is RoomListEvents.UpdateFilter -> roomListDataSource.updateFilter(event.newFilter) is RoomListEvents.UpdateVisibleRange -> updateVisibleRange(event.range) RoomListEvents.DismissRequestVerificationPrompt -> verificationPromptDismissed = true RoomListEvents.DismissRecoveryKeyPrompt -> recoveryKeyPromptDismissed = true - RoomListEvents.ToggleSearchResults -> { - if (displaySearchResults) { - roomListDataSource.updateFilter("") - } - displaySearchResults = !displaySearchResults - } + RoomListEvents.ToggleSearchResults -> searchState.eventSink(RoomListSearchEvents.ToggleSearchVisibility) is RoomListEvents.ShowContextMenu -> { coroutineScope.showContextMenu(event, contextMenu) } @@ -175,16 +170,14 @@ class RoomListPresenter @Inject constructor( matrixUser = matrixUser.value, showAvatarIndicator = showAvatarIndicator, roomList = roomList, - filter = filter, - filteredRoomList = filteredRoomList, displayVerificationPrompt = displayVerificationPrompt, displayRecoveryKeyPrompt = displayRecoveryKeyPrompt, snackbarMessage = snackbarMessage, hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online, invitesState = inviteStateDataSource.inviteState(), - displaySearchResults = displaySearchResults, contextMenu = contextMenu.value, leaveRoomState = leaveRoomState, + searchState = searchState, displayMigrationStatus = isMigrating, eventSink = ::handleEvents ) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt index e97ab7c073..3fffd522ae 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt @@ -19,6 +19,7 @@ package io.element.android.features.roomlist.impl import androidx.compose.runtime.Immutable import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.roomlist.impl.model.RoomListRoomSummary +import io.element.android.features.roomlist.impl.search.RoomListSearchState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.matrix.api.core.RoomId @@ -30,16 +31,14 @@ data class RoomListState( val matrixUser: MatrixUser?, val showAvatarIndicator: Boolean, val roomList: AsyncData>, - val filter: String?, - val filteredRoomList: ImmutableList, val displayVerificationPrompt: Boolean, val displayRecoveryKeyPrompt: Boolean, val hasNetworkConnection: Boolean, val snackbarMessage: SnackbarMessage?, val invitesState: InvitesState, - val displaySearchResults: Boolean, val contextMenu: ContextMenu, val leaveRoomState: LeaveRoomState, + val searchState: RoomListSearchState, val displayMigrationStatus: Boolean, val eventSink: (RoomListEvents) -> Unit, ) { diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt index c54360d3d3..ed1a948400 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt @@ -21,6 +21,7 @@ import io.element.android.features.leaveroom.api.aLeaveRoomState import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.features.roomlist.impl.model.aRoomListRoomSummary +import io.element.android.features.roomlist.impl.search.aRoomListSearchState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize @@ -41,14 +42,13 @@ open class RoomListStateProvider : PreviewParameterProvider { aRoomListState().copy(hasNetworkConnection = false), aRoomListState().copy(invitesState = InvitesState.SeenInvites), aRoomListState().copy(invitesState = InvitesState.NewInvites), - aRoomListState().copy(displaySearchResults = true, filter = "", filteredRoomList = persistentListOf()), - aRoomListState().copy(displaySearchResults = true), aRoomListState().copy(contextMenu = aContextMenuShown(roomName = "A nice room name")), aRoomListState().copy(contextMenu = aContextMenuShown(isFavorite = true)), aRoomListState().copy(displayRecoveryKeyPrompt = true), aRoomListState().copy(roomList = AsyncData.Success(persistentListOf())), aRoomListState().copy(roomList = AsyncData.Loading(prevData = RoomListRoomSummaryFactory.createFakeList())), aRoomListState().copy(matrixUser = null, displayMigrationStatus = true), + aRoomListState().copy(searchState = aRoomListSearchState(isSearchActive = true, query = "Test")), ) } @@ -56,16 +56,14 @@ internal fun aRoomListState() = RoomListState( matrixUser = MatrixUser(userId = UserId("@id:domain"), displayName = "User#1"), showAvatarIndicator = false, roomList = AsyncData.Success(aRoomListRoomSummaryList()), - filter = "filter", - filteredRoomList = aRoomListRoomSummaryList(), hasNetworkConnection = true, snackbarMessage = null, displayVerificationPrompt = false, displayRecoveryKeyPrompt = false, invitesState = InvitesState.NoInvites, - displaySearchResults = false, contextMenu = RoomListState.ContextMenu.Hidden, leaveRoomState = aLeaveRoomState(), + searchState = aRoomListSearchState(), displayMigrationStatus = false, eventSink = {} ) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt index a9fcda5428..fd8c4a7ccc 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt @@ -57,7 +57,7 @@ import io.element.android.features.roomlist.impl.components.RoomListTopBar import io.element.android.features.roomlist.impl.components.RoomSummaryRow import io.element.android.features.roomlist.impl.migration.MigrationScreenView import io.element.android.features.roomlist.impl.model.RoomListRoomSummary -import io.element.android.features.roomlist.impl.search.RoomListSearchResultView +import io.element.android.features.roomlist.impl.search.RoomListSearchView import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -118,8 +118,8 @@ fun RoomListView( onMenuActionClicked = onMenuActionClicked, ) // This overlaid view will only be visible when state.displaySearchResults is true - RoomListSearchResultView( - state = state, + RoomListSearchView( + state = state.searchState, onRoomClicked = onRoomClicked, onRoomLongClicked = { onRoomLongClicked(it) }, modifier = Modifier @@ -207,8 +207,7 @@ private fun RoomListContent( RoomListTopBar( matrixUser = state.matrixUser, showAvatarIndicator = state.showAvatarIndicator, - areSearchResultsDisplayed = state.displaySearchResults, - onFilterChanged = { state.eventSink(RoomListEvents.UpdateFilter(it)) }, + areSearchResultsDisplayed = state.searchState.isSearchActive, onToggleSearch = { state.eventSink(RoomListEvents.ToggleSearchResults) }, onMenuActionClicked = onMenuActionClicked, onOpenSettings = onOpenSettings, diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt index 871ca846c7..0d0a750a9f 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt @@ -16,7 +16,6 @@ package io.element.android.features.roomlist.impl.components -import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth @@ -87,7 +86,6 @@ fun RoomListTopBar( matrixUser: MatrixUser?, showAvatarIndicator: Boolean, areSearchResultsDisplayed: Boolean, - onFilterChanged: (String) -> Unit, onToggleSearch: () -> Unit, onMenuActionClicked: (RoomListMenuAction) -> Unit, onOpenSettings: () -> Unit, @@ -95,15 +93,6 @@ fun RoomListTopBar( displayMenuItems: Boolean, modifier: Modifier = Modifier, ) { - fun closeFilter() { - onFilterChanged("") - } - - BackHandler(enabled = areSearchResultsDisplayed) { - closeFilter() - onToggleSearch() - } - DefaultRoomListTopBar( matrixUser = matrixUser, showAvatarIndicator = showAvatarIndicator, 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 c55a47596b..da03122134 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 @@ -25,15 +25,11 @@ 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.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -54,9 +50,7 @@ class RoomListDataSource @Inject constructor( observeNotificationSettings() } - private val _filter = MutableStateFlow("") private val _allRooms = MutableSharedFlow>(replay = 1) - private val _filteredRooms = MutableStateFlow>(persistentListOf()) private val lock = Mutex() private val diffCache = MutableListDiffCache() @@ -72,29 +66,9 @@ class RoomListDataSource @Inject constructor( replaceWith(roomSummaries) } .launchIn(coroutineScope) - - combine( - _filter, - _allRooms - ) { filterValue, allRoomsValue -> - when { - filterValue.isEmpty() -> emptyList() - else -> allRoomsValue.filter { it.name.contains(filterValue, ignoreCase = true) } - }.toImmutableList() - } - .onEach { - _filteredRooms.value = it - } - .launchIn(coroutineScope) } - fun updateFilter(filterValue: String) { - _filter.value = filterValue - } - - val filter: StateFlow = _filter val allRooms: SharedFlow> = _allRooms - val filteredRooms: StateFlow> = _filteredRooms @OptIn(FlowPreview::class) private fun observeNotificationSettings() { diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchEvents.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchEvents.kt new file mode 100644 index 0000000000..6c99a4e0d0 --- /dev/null +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchEvents.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024 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.features.roomlist.impl.search + +sealed interface RoomListSearchEvents { + data object ToggleSearchVisibility : RoomListSearchEvents + data class QueryChanged(val query: String) : RoomListSearchEvents + data object ClearQuery : RoomListSearchEvents +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenter.kt new file mode 100644 index 0000000000..5beaa8077a --- /dev/null +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenter.kt @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2024 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.features.roomlist.impl.search + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +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.RoomListService +import io.element.android.libraries.matrix.api.roomlist.RoomSummary +import io.element.android.libraries.matrix.api.roomlist.loadAllIncrementally +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +private const val PAGE_SIZE = 50 + +class RoomListSearchPresenter @Inject constructor( + private val roomListService: RoomListService, + private val roomSummaryFactory: RoomListRoomSummaryFactory, + private val coroutineDispatchers: CoroutineDispatchers, +) : Presenter { + + @Composable + override fun present(): RoomListSearchState { + + var isSearchActive by rememberSaveable { + mutableStateOf(false) + } + var searchQuery by rememberSaveable { + mutableStateOf("") + } + val coroutineScope = rememberCoroutineScope() + + val roomList = remember { + roomListService.createRoomList( + coroutineScope = coroutineScope, + pageSize = PAGE_SIZE, + initialFilter = RoomListFilter.all(RoomListFilter.None), + source = RoomList.Source.All, + ) + } + + LaunchedEffect(Unit) { + roomList.loadAllIncrementally(this) + } + LaunchedEffect(key1 = searchQuery) { + val filter = if (searchQuery.isBlank()) { + RoomListFilter.all(RoomListFilter.None) + } else { + RoomListFilter.all(RoomListFilter.NonLeft, RoomListFilter.NormalizedMatchRoomName(searchQuery)) + } + roomList.updateFilter(filter) + } + + fun handleEvents(event: RoomListSearchEvents) { + when (event) { + RoomListSearchEvents.ClearQuery -> { + searchQuery = "" + } + is RoomListSearchEvents.QueryChanged -> { + searchQuery = event.query + } + RoomListSearchEvents.ToggleSearchVisibility -> { + isSearchActive = !isSearchActive + searchQuery = "" + } + } + } + + val searchResults by roomList + .rememberMappedSummaries() + .collectAsState(initial = persistentListOf()) + + return RoomListSearchState( + isSearchActive = isSearchActive, + query = searchQuery, + results = searchResults, + eventSink = ::handleEvents + ) + } + + @Composable + private fun RoomList.rememberMappedSummaries() = remember { + summaries + .map { roomSummaries -> + roomSummaries + .filterIsInstance() + .map(roomSummaryFactory::create) + .toPersistentList() + } + .flowOn(coroutineDispatchers.computation) + } +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchState.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchState.kt new file mode 100644 index 0000000000..c4b24dc798 --- /dev/null +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchState.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 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.features.roomlist.impl.search + +import io.element.android.features.roomlist.impl.model.RoomListRoomSummary +import kotlinx.collections.immutable.ImmutableList + +data class RoomListSearchState( + val isSearchActive: Boolean, + val query: String, + val results: ImmutableList, + val eventSink: (RoomListSearchEvents) -> Unit +) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchStateProvider.kt new file mode 100644 index 0000000000..b846354b6f --- /dev/null +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchStateProvider.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024 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.features.roomlist.impl.search + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.roomlist.impl.aRoomListRoomSummaryList +import io.element.android.features.roomlist.impl.model.RoomListRoomSummary +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +class RoomListSearchStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aRoomListSearchState(), + aRoomListSearchState( + isSearchActive = true, + query = "Test", + results = aRoomListRoomSummaryList() + ), + ) +} + +fun aRoomListSearchState( + isSearchActive: Boolean = false, + query: String = "", + results: ImmutableList = persistentListOf(), +) = RoomListSearchState( + isSearchActive = isSearchActive, + query = query, + results = results, + eventSink = { }, +) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchResultView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchView.kt similarity index 73% rename from features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchResultView.kt rename to features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchView.kt index 2cf63393f9..eff6449449 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchResultView.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchView.kt @@ -16,6 +16,7 @@ package io.element.android.features.roomlist.impl.search +import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -25,32 +26,23 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.Velocity +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.tokens.generated.CompoundIcons -import io.element.android.features.roomlist.impl.RoomListEvents -import io.element.android.features.roomlist.impl.RoomListState -import io.element.android.features.roomlist.impl.aRoomListState import io.element.android.features.roomlist.impl.components.RoomSummaryRow import io.element.android.features.roomlist.impl.contentType import io.element.android.features.roomlist.impl.model.RoomListRoomSummary @@ -68,26 +60,30 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.ui.strings.CommonStrings @Composable -internal fun RoomListSearchResultView( - state: RoomListState, +internal fun RoomListSearchView( + state: RoomListSearchState, onRoomClicked: (RoomId) -> Unit, onRoomLongClicked: (RoomListRoomSummary) -> Unit, modifier: Modifier = Modifier, ) { + BackHandler(enabled = state.isSearchActive) { + state.eventSink(RoomListSearchEvents.ToggleSearchVisibility) + } + AnimatedVisibility( - visible = state.displaySearchResults, + visible = state.isSearchActive, enter = fadeIn(), exit = fadeOut(), ) { Column( modifier = modifier - .applyIf(state.displaySearchResults, ifTrue = { + .applyIf(state.isSearchActive, ifTrue = { // Disable input interaction to underlying views pointerInput(Unit) {} }) ) { - if (state.displaySearchResults) { - RoomListSearchResultContent( + if (state.isSearchActive) { + RoomListSearchContent( state = state, onRoomClicked = onRoomClicked, onRoomLongClicked = onRoomLongClicked, @@ -99,15 +95,15 @@ internal fun RoomListSearchResultView( @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun RoomListSearchResultContent( - state: RoomListState, +private fun RoomListSearchContent( + state: RoomListSearchState, onRoomClicked: (RoomId) -> Unit, onRoomLongClicked: (RoomListRoomSummary) -> Unit, ) { val borderColor = MaterialTheme.colorScheme.tertiary val strokeWidth = 1.dp fun onBackButtonPressed() { - state.eventSink(RoomListEvents.ToggleSearchResults) + state.eventSink(RoomListSearchEvents.ToggleSearchVisibility) } fun onRoomClicked(room: RoomListRoomSummary) { @@ -126,7 +122,7 @@ private fun RoomListSearchResultContent( }, navigationIcon = { BackButton(onClick = ::onBackButtonPressed) }, title = { - val filter = state.filter.orEmpty() + val filter = state.query val focusRequester = FocusRequester() TextField( modifier = Modifier @@ -134,7 +130,7 @@ private fun RoomListSearchResultContent( .focusRequester(focusRequester), value = filter, singleLine = true, - onValueChange = { state.eventSink(RoomListEvents.UpdateFilter(it)) }, + onValueChange = { state.eventSink(RoomListSearchEvents.QueryChanged(it)) }, colors = TextFieldDefaults.colors( focusedContainerColor = Color.Transparent, unfocusedContainerColor = Color.Transparent, @@ -147,7 +143,7 @@ private fun RoomListSearchResultContent( trailingIcon = { if (filter.isNotEmpty()) { IconButton(onClick = { - state.eventSink(RoomListEvents.UpdateFilter("")) + state.eventSink(RoomListSearchEvents.ClearQuery) }) { Icon( imageVector = CompoundIcons.Close(), @@ -158,8 +154,8 @@ private fun RoomListSearchResultContent( } ) - LaunchedEffect(state.displaySearchResults) { - if (state.displaySearchResults) { + LaunchedEffect(state.isSearchActive) { + if (state.isSearchActive) { focusRequester.requestFocus() } } @@ -168,39 +164,16 @@ private fun RoomListSearchResultContent( ) } ) { padding -> - val lazyListState = rememberLazyListState() - val visibleRange by remember { - derivedStateOf { - val layoutInfo = lazyListState.layoutInfo - val firstItemIndex = layoutInfo.visibleItemsInfo.firstOrNull()?.index ?: 0 - val size = layoutInfo.visibleItemsInfo.size - firstItemIndex until firstItemIndex + size - } - } - val nestedScrollConnection = remember { - object : NestedScrollConnection { - override suspend fun onPostFling( - consumed: Velocity, - available: Velocity - ): Velocity { - state.eventSink(RoomListEvents.UpdateVisibleRange(visibleRange)) - return super.onPostFling(consumed, available) - } - } - } Column( modifier = Modifier .padding(padding) .consumeWindowInsets(padding) ) { LazyColumn( - modifier = Modifier - .weight(1f) - .nestedScroll(nestedScrollConnection), - state = lazyListState, + modifier = Modifier.weight(1f), ) { items( - items = state.filteredRoomList, + items = state.results, contentType = { room -> room.contentType() }, ) { room -> RoomSummaryRow( @@ -216,9 +189,9 @@ private fun RoomListSearchResultContent( @PreviewsDayNight @Composable -internal fun RoomListSearchResultContentPreview() = ElementPreview { - RoomListSearchResultContent( - state = aRoomListState(), +internal fun RoomListSearchResultContentPreview(@PreviewParameter(RoomListSearchStateProvider::class) state: RoomListSearchState) = ElementPreview { + RoomListSearchContent( + state = state, onRoomClicked = {}, onRoomLongClicked = {} ) diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt index f106b72fb9..b2ad2fee23 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt @@ -33,6 +33,8 @@ import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryF import io.element.android.features.roomlist.impl.migration.InMemoryMigrationScreenStore import io.element.android.features.roomlist.impl.migration.MigrationScreenPresenter import io.element.android.features.roomlist.impl.model.createRoomListRoomSummary +import io.element.android.features.roomlist.impl.search.RoomListSearchPresenter +import io.element.android.features.roomlist.impl.search.createRoomListSearchPresenter import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter import io.element.android.libraries.dateformatter.test.A_FORMATTED_DATE import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter @@ -54,7 +56,6 @@ import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatu import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.A_ROOM_ID -import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_NAME @@ -145,24 +146,6 @@ class RoomListPresenterTests { } } - @Test - fun `present - should filter room with success`() = runTest { - val scope = CoroutineScope(coroutineContext + SupervisorJob()) - val presenter = createRoomListPresenter(coroutineScope = scope) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - skipItems(1) - val withUserState = awaitItem() - assertThat(withUserState.filter).isEqualTo("") - withUserState.eventSink.invoke(RoomListEvents.UpdateFilter("t")) - val withFilterState = awaitItem() - assertThat(withFilterState.filter).isEqualTo("t") - cancelAndIgnoreRemainingEvents() - scope.cancel() - } - } - @Test fun `present - load 1 room with success`() = runTest { val roomListService = FakeRoomListService() @@ -196,51 +179,7 @@ class RoomListPresenterTests { numberOfUnreadMessages = 2, ) ) - scope.cancel() - } - } - - @Test - fun `present - load 1 room with success and filter rooms`() = runTest { - val roomListService = FakeRoomListService() - val matrixClient = FakeMatrixClient( - roomListService = roomListService - ) - val scope = CoroutineScope(coroutineContext + SupervisorJob()) - val presenter = createRoomListPresenter(client = matrixClient, coroutineScope = scope) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - roomListService.postAllRooms( - listOf( - aRoomSummaryFilled( - numUnreadMentions = 1, - numUnreadMessages = 2, - ) - ) - ) - skipItems(3) - val loadedState = awaitItem() - // Test filtering with result - assertThat(loadedState.roomList.dataOrNull().orEmpty().size).isEqualTo(1) - loadedState.eventSink.invoke(RoomListEvents.UpdateFilter(A_ROOM_NAME.substring(0, 3))) - skipItems(1) - val withFilteredRoomState = awaitItem() - assertThat(withFilteredRoomState.filteredRoomList.size).isEqualTo(1) - assertThat(withFilteredRoomState.filter).isEqualTo(A_ROOM_NAME.substring(0, 3)) - assertThat(withFilteredRoomState.filteredRoomList.size).isEqualTo(1) - assertThat(withFilteredRoomState.filteredRoomList.first()).isEqualTo( - createRoomListRoomSummary( - numberOfUnreadMentions = 1, - numberOfUnreadMessages = 2, - ) - ) - // Test filtering without result - withFilteredRoomState.eventSink.invoke(RoomListEvents.UpdateFilter("tada")) - skipItems(1) - val withNotFilteredRoomState = awaitItem() - assertThat(withNotFilteredRoomState.filter).isEqualTo("tada") - assertThat(withNotFilteredRoomState.filteredRoomList).isEmpty() + cancelAndIgnoreRemainingEvents() scope.cancel() } } @@ -572,7 +511,8 @@ class RoomListPresenterTests { migrationScreenPresenter: MigrationScreenPresenter = MigrationScreenPresenter( matrixClient = client, migrationScreenStore = InMemoryMigrationScreenStore(), - ) + ), + searchPresenter: RoomListSearchPresenter = createRoomListSearchPresenter(roomListService = client.roomListService) ) = RoomListPresenter( client = client, sessionVerificationService = sessionVerificationService, @@ -598,6 +538,7 @@ class RoomListPresenterTests { featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.SecureStorage.key to true)), ), migrationScreenPresenter = migrationScreenPresenter, + searchPresenter = searchPresenter, sessionPreferencesStore = sessionPreferencesStore, ) } diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenterTests.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenterTests.kt new file mode 100644 index 0000000000..fb124356e1 --- /dev/null +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenterTests.kt @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2024 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.features.roomlist.impl.search + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory +import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter +import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter +import io.element.android.libraries.matrix.api.roomlist.RoomListFilter +import io.element.android.libraries.matrix.api.roomlist.RoomListService +import io.element.android.libraries.matrix.api.roomlist.RoomSummary +import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled +import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class RoomListSearchPresenterTests { + @Test + fun `present - initial state`() = runTest { + val presenter = createRoomListSearchPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().let { state -> + assertThat(state.isSearchActive).isFalse() + assertThat(state.query).isEmpty() + assertThat(state.results).isEmpty() + } + } + } + + @Test + fun `present - toggle search visibility`() = runTest { + val presenter = createRoomListSearchPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().let { state -> + assertThat(state.isSearchActive).isFalse() + state.eventSink(RoomListSearchEvents.ToggleSearchVisibility) + } + awaitItem().let { state -> + assertThat(state.isSearchActive).isTrue() + state.eventSink(RoomListSearchEvents.ToggleSearchVisibility) + } + awaitItem().let { state -> + assertThat(state.isSearchActive).isFalse() + } + } + } + + @Test + fun `present - query search changes`() = runTest { + val roomListService = FakeRoomListService() + val presenter = createRoomListSearchPresenter(roomListService) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().let { state -> + assertThat( + roomListService.allRooms.currentFilter.value + ).isEqualTo( + RoomListFilter.all( + RoomListFilter.None, + ) + ) + state.eventSink(RoomListSearchEvents.QueryChanged("Search")) + } + awaitItem().let { state -> + assertThat(state.query).isEqualTo("Search") + assertThat( + roomListService.allRooms.currentFilter.value + ).isEqualTo( + RoomListFilter.all( + RoomListFilter.NonLeft, + RoomListFilter.NormalizedMatchRoomName("Search") + ) + ) + state.eventSink(RoomListSearchEvents.ClearQuery) + } + awaitItem().let { state -> + assertThat(state.query).isEmpty() + assertThat( + roomListService.allRooms.currentFilter.value + ).isEqualTo( + RoomListFilter.all( + RoomListFilter.None, + ) + ) + } + } + } + + @Test + fun `present - room list changes`() = runTest { + val roomListService = FakeRoomListService() + val presenter = createRoomListSearchPresenter(roomListService) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().let { state -> + assertThat(state.results).isEmpty() + } + roomListService.postAllRooms( + listOf( + RoomSummary.Empty("1"), + aRoomSummaryFilled() + ) + ) + awaitItem().let { state -> + assertThat(state.results).hasSize(1) + } + roomListService.postAllRooms(emptyList()) + awaitItem().let { state -> + assertThat(state.results).isEmpty() + } + } + } +} + +fun TestScope.createRoomListSearchPresenter( + roomListService: RoomListService = FakeRoomListService(), +): RoomListSearchPresenter { + return RoomListSearchPresenter( + roomListService = roomListService, + roomSummaryFactory = RoomListRoomSummaryFactory( + lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter(), + roomLastMessageFormatter = FakeRoomLastMessageFormatter(), + ), + coroutineDispatchers = testCoroutineDispatchers(), + ) +} 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 5ffc58c332..9b42fbc70b 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 @@ -33,6 +33,11 @@ interface RoomList { data class Loaded(val numberOfRooms: Int) : LoadingState } + enum class Source { + All, + Invites, + } + /** * The list of room summaries as a flow. */ diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListFilter.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListFilter.kt index 99ba4531e2..41f5240a19 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListFilter.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListFilter.kt @@ -18,38 +18,68 @@ package io.element.android.libraries.matrix.api.roomlist sealed interface RoomListFilter { companion object { + /** + * Create a filter that matches all the given filters. + */ fun all(vararg filters: RoomListFilter): RoomListFilter { return All(filters.toList()) } + /** + * Create a filter that matches any of the given filters. + */ fun any(vararg filters: RoomListFilter): RoomListFilter { return Any(filters.toList()) } } + /** + * A filter that matches all the given filters. + */ data class All( val filters: List ) : RoomListFilter + /** + * A filter that matches any of the given filters. + */ data class Any( val filters: List ) : RoomListFilter + /** + * A filter that matches rooms that are not left. + */ data object NonLeft : RoomListFilter + /** + * A filter that matches rooms that are unread. + */ data object Unread : RoomListFilter + /** + * A filter that matches either Group or People rooms. + */ sealed interface Category : RoomListFilter { data object Group : Category data object People : Category } + /** + * A filter that matches no room. + */ data object None : RoomListFilter + /** + * A filter that matches rooms with a name using a normalized match. + */ data class NormalizedMatchRoomName( val pattern: String ) : RoomListFilter + /** + * A filter that matches rooms with a name using a fuzzy match. + */ data class FuzzyMatchRoomName( val pattern: String ) : RoomListFilter 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 c13e6ecad9..04018b7bea 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 @@ -17,6 +17,7 @@ package io.element.android.libraries.matrix.api.roomlist import androidx.compose.runtime.Immutable +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.StateFlow /** @@ -39,6 +40,20 @@ interface RoomListService { data object Hide : SyncIndicator } + /** + * Creates a room list that can be used to load more rooms and filter them dynamically. + * @param coroutineScope the scope to use for the room list. When the scope will be closed, the room list will be closed too. + * @param pageSize the number of rooms to load at once. + * @param initialFilter the initial filter to apply to the rooms. + * @param source the source of the rooms, either all rooms or invites. + */ + fun createRoomList( + coroutineScope: CoroutineScope, + pageSize: Int, + initialFilter: RoomListFilter, + source: RoomList.Source, + ): DynamicRoomList + /** * returns a [DynamicRoomList] object of all rooms we want to display. * This will exclude some rooms like the invites, or spaces. 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 f7213becc4..34d479e394 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 @@ -199,8 +199,8 @@ class RustMatrixClient( sessionCoroutineScope = sessionCoroutineScope, roomListFactory = RoomListFactory( innerRoomListService = innerRoomListService, - coroutineScope = sessionCoroutineScope, - dispatcher = sessionDispatcher, + defaultCoroutineScope = sessionCoroutineScope, + defaultCoroutineContext = sessionDispatcher, ), ) 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 f0d9c7e2a4..1c77c5c5c5 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 @@ -20,7 +20,6 @@ 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.RoomListFilter 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 @@ -30,13 +29,15 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.matrix.rustcomponents.sdk.RoomListLoadingState +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext 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 defaultCoroutineScope: CoroutineScope, + private val defaultCoroutineContext: CoroutineContext = EmptyCoroutineContext, private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(), ) { /** @@ -44,18 +45,21 @@ internal class RoomListFactory( */ fun createRoomList( pageSize: Int, + coroutineScope: CoroutineScope = defaultCoroutineScope, + coroutineContext: CoroutineContext = defaultCoroutineContext, initialFilter: RoomListFilter = RoomListFilter.all(), innerProvider: suspend () -> InnerRoomList ): DynamicRoomList { val loadingStateFlow: MutableStateFlow = MutableStateFlow(RoomList.LoadingState.NotLoaded) val summariesFlow = MutableStateFlow>(emptyList()) - val processor = RoomSummaryListProcessor(summariesFlow, innerRoomListService, dispatcher, roomSummaryDetailsFactory) + val processor = RoomSummaryListProcessor(summariesFlow, innerRoomListService, coroutineContext, roomSummaryDetailsFactory) // 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) { + + coroutineScope.launch(coroutineContext) { innerRoomList = innerProvider() innerRoomList?.let { innerRoomList -> innerRoomList.entriesFlow( 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 5525d802bf..a5028bb614 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 @@ -17,7 +17,6 @@ package io.element.android.libraries.matrix.impl.roomlist import io.element.android.libraries.matrix.api.roomlist.RoomSummary -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -28,11 +27,12 @@ import org.matrix.rustcomponents.sdk.RoomListServiceInterface import org.matrix.rustcomponents.sdk.use import timber.log.Timber import java.util.UUID +import kotlin.coroutines.CoroutineContext class RoomSummaryListProcessor( private val roomSummaries: MutableStateFlow>, private val roomListService: RoomListServiceInterface, - private val dispatcher: CoroutineDispatcher, + private val coroutineContext: CoroutineContext, private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(), ) { private val roomSummariesByIdentifier = HashMap() @@ -130,7 +130,7 @@ class RoomSummaryListProcessor( return builtRoomSummary } - private suspend fun updateRoomSummaries(block: suspend MutableList.() -> Unit) = withContext(dispatcher) { + private suspend fun updateRoomSummaries(block: suspend MutableList.() -> Unit) = withContext(coroutineContext) { mutex.withLock { val mutableRoomSummaries = roomSummaries.value.toMutableList() block(mutableRoomSummaries) 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 4fef34b571..0c48dc2a2f 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 @@ -42,8 +42,26 @@ private const val DEFAULT_PAGE_SIZE = 20 internal class RustRoomListService( private val innerRoomListService: InnerRustRoomListService, private val sessionCoroutineScope: CoroutineScope, - roomListFactory: RoomListFactory, + private val roomListFactory: RoomListFactory, ) : RoomListService { + override fun createRoomList( + coroutineScope: CoroutineScope, + pageSize: Int, + initialFilter: RoomListFilter, + source: RoomList.Source + ): DynamicRoomList { + return roomListFactory.createRoomList( + pageSize = pageSize, + initialFilter = initialFilter, + coroutineScope = coroutineScope, + ) { + when (source) { + RoomList.Source.All -> innerRoomListService.allRooms() + RoomList.Source.Invites -> innerRoomListService.invites() + } + } + } + override val allRooms: DynamicRoomList = roomListFactory.createRoomList( pageSize = DEFAULT_PAGE_SIZE, initialFilter = RoomListFilter.all(RoomListFilter.NonLeft), 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 a9b0fea454..3812605546 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 @@ -158,7 +158,7 @@ class RoomSummaryListProcessorTests { private fun TestScope.createProcessor() = RoomSummaryListProcessor( summaries, fakeRoomListService, - dispatcher = StandardTestDispatcher(testScheduler), + coroutineContext = StandardTestDispatcher(testScheduler), roomSummaryDetailsFactory = RoomSummaryDetailsFactory(), ) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt index a1ef41b742..e0b0c38d4d 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt @@ -53,6 +53,10 @@ fun aRoomSummaryFilled( ) ) +fun aRoomSummaryFilled( + details: RoomSummaryDetails = aRoomSummaryDetails(), +) = RoomSummary.Filled(details) + fun aRoomSummaryDetails( roomId: RoomId = A_ROOM_ID, name: String = A_ROOM_NAME, 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 7540d6cee8..d53596e462 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 @@ -21,6 +21,7 @@ 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.RoomListService import io.element.android.libraries.matrix.api.roomlist.RoomSummary +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -59,13 +60,20 @@ class FakeRoomListService : RoomListService { var latestSlidingSyncRange: IntRange? = null private set - override val allRooms: DynamicRoomList = SimplePagedRoomList( + override fun createRoomList(coroutineScope: CoroutineScope, pageSize: Int, initialFilter: RoomListFilter, source: RoomList.Source): DynamicRoomList { + return when (source) { + RoomList.Source.All -> allRooms + RoomList.Source.Invites -> invites + } + } + + override val allRooms = SimplePagedRoomList( allRoomSummariesFlow, allRoomsLoadingStateFlow, MutableStateFlow(RoomListFilter.all()) ) - override val invites: RoomList = SimplePagedRoomList( + override val invites = SimplePagedRoomList( inviteRoomSummariesFlow, inviteRoomsLoadingStateFlow, MutableStateFlow(RoomListFilter.all()) 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 4f1b07ce69..5ff9ed08bf 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 @@ -25,7 +25,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.getAndUpdate data class SimplePagedRoomList( - override val summaries: StateFlow>, + override val summaries: MutableStateFlow>, override val loadingState: StateFlow, override val currentFilter: MutableStateFlow ) : DynamicRoomList {