diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt index c81f4f773d..6827055b2c 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt @@ -30,8 +30,6 @@ import io.element.android.features.lockscreen.impl.pin.model.assertText import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.test.FakeMatrixClient -import io.element.android.tests.testutils.awaitLastSequentialItem -import io.element.android.tests.testutils.consumeItemsUntilPredicate import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.test.runTest import org.junit.Test @@ -54,13 +52,14 @@ class PinUnlockPresenterTest { assertThat(state.signOutAction).isInstanceOf(AsyncData.Uninitialized::class.java) assertThat(state.remainingAttempts).isInstanceOf(AsyncData.Uninitialized::class.java) } - consumeItemsUntilPredicate { - it.pinEntry is AsyncData.Success && it.remainingAttempts is AsyncData.Success - }.last().also { state -> + awaitItem().also { state -> + assertThat(state.pinEntry).isInstanceOf(AsyncData.Success::class.java) + assertThat(state.remainingAttempts).isInstanceOf(AsyncData.Success::class.java) state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('1'))) state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('2'))) } - awaitLastSequentialItem().also { state -> + skipItems(1) + awaitItem().also { state -> state.pinEntry.assertText(halfCompletePin) state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('3'))) state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Back)) @@ -68,7 +67,8 @@ class PinUnlockPresenterTest { state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('3'))) state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('5'))) } - awaitLastSequentialItem().also { state -> + skipItems(4) + awaitItem().also { state -> state.pinEntry.assertText(completePin) assertThat(state.isUnlocked).isTrue() } @@ -81,9 +81,11 @@ class PinUnlockPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = consumeItemsUntilPredicate { - it.pinEntry is AsyncData.Success && it.remainingAttempts is AsyncData.Success - }.last() + skipItems(1) + val initialState = awaitItem().also { state -> + assertThat(state.pinEntry).isInstanceOf(AsyncData.Success::class.java) + assertThat(state.remainingAttempts).isInstanceOf(AsyncData.Success::class.java) + } val numberOfAttempts = initialState.remainingAttempts.dataOrNull() ?: 0 repeat(numberOfAttempts) { initialState.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('1'))) @@ -91,7 +93,8 @@ class PinUnlockPresenterTest { initialState.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('3'))) initialState.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('4'))) } - awaitLastSequentialItem().also { state -> + skipItems(4 * numberOfAttempts + 2) + awaitItem().also { state -> assertThat(state.remainingAttempts.dataOrNull()).isEqualTo(0) assertThat(state.showSignOutPrompt).isTrue() assertThat(state.isSignOutPromptCancellable).isFalse() @@ -105,26 +108,28 @@ class PinUnlockPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - consumeItemsUntilPredicate { - it.pinEntry is AsyncData.Success && it.remainingAttempts is AsyncData.Success - }.last().also { state -> + skipItems(1) + awaitItem().also { state -> + assertThat(state.pinEntry).isInstanceOf(AsyncData.Success::class.java) + assertThat(state.remainingAttempts).isInstanceOf(AsyncData.Success::class.java) state.eventSink(PinUnlockEvents.OnForgetPin) } - awaitLastSequentialItem().also { state -> + awaitItem().also { state -> assertThat(state.showSignOutPrompt).isTrue() assertThat(state.isSignOutPromptCancellable).isTrue() state.eventSink(PinUnlockEvents.ClearSignOutPrompt) } - awaitLastSequentialItem().also { state -> + awaitItem().also { state -> assertThat(state.showSignOutPrompt).isFalse() state.eventSink(PinUnlockEvents.OnForgetPin) } - awaitLastSequentialItem().also { state -> + awaitItem().also { state -> assertThat(state.showSignOutPrompt).isTrue() state.eventSink(PinUnlockEvents.SignOut) } - consumeItemsUntilPredicate { state -> - state.signOutAction is AsyncData.Success + skipItems(2) + awaitItem().also { state -> + assertThat(state.signOutAction).isInstanceOf(AsyncData.Success::class.java) } } } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersPresenter.kt index 56cc72e1a3..090b4e868a 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersPresenter.kt @@ -83,7 +83,7 @@ class RoomListFiltersPresenter @Inject constructor( RoomListFilter.Unread -> MatrixRoomListFilter.Unread RoomListFilter.Favourites -> MatrixRoomListFilter.Favorite } - }.plus(MatrixRoomListFilter.NonLeft) + } ) roomListService.allRooms.updateFilter(allRoomsFilter) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersView.kt index 8aefe30d0c..efabd5cf28 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersView.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersView.kt @@ -103,10 +103,8 @@ private fun LazyListScope.roomListFilters( ) { items( items = filters, - key = { it.ordinal }, ) { filter -> RoomListFilterView( - modifier = Modifier.animateItemPlacement(), roomListFilter = filter, selected = selected, onClick = onClick, diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchDataSource.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchDataSource.kt new file mode 100644 index 0000000000..59d8bf9f13 --- /dev/null +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchDataSource.kt @@ -0,0 +1,73 @@ +/* + * 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.datasource.RoomListRoomSummaryFactory +import io.element.android.features.roomlist.impl.model.RoomListRoomSummary +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.PersistentList +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +private const val PAGE_SIZE = 30 + +class RoomListSearchDataSource @Inject constructor( + roomListService: RoomListService, + coroutineDispatchers: CoroutineDispatchers, + private val roomSummaryFactory: RoomListRoomSummaryFactory, +) { + private val roomList = roomListService.createRoomList( + pageSize = PAGE_SIZE, + initialFilter = RoomListFilter.None, + source = RoomList.Source.All, + ) + + val roomSummaries: Flow> = roomList.summaries + .map { roomSummaries -> + roomSummaries + .filterIsInstance() + .map(roomSummaryFactory::create) + .toPersistentList() + } + .flowOn(coroutineDispatchers.computation) + + suspend fun setIsActive(isActive: Boolean) = coroutineScope { + if (isActive) { + roomList.loadAllIncrementally(this) + } else { + roomList.reset() + } + } + + suspend fun setSearchQuery(searchQuery: String) = coroutineScope { + val filter = if (searchQuery.isBlank()) { + RoomListFilter.None + } else { + RoomListFilter.NormalizedMatchRoomName(searchQuery) + } + roomList.updateFilter(filter) + } +} 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 index 6917a505c3..78bcda07f1 100644 --- 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 @@ -21,30 +21,14 @@ 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, + private val dataSource: RoomListSearchDataSource, ) : Presenter { @Composable override fun present(): RoomListSearchState { @@ -54,27 +38,13 @@ class RoomListSearchPresenter @Inject constructor( 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(isSearchActive) { + dataSource.setIsActive(isSearchActive) } - 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) + LaunchedEffect(searchQuery) { + dataSource.setSearchQuery(searchQuery) } fun handleEvents(event: RoomListSearchEvents) { @@ -92,9 +62,7 @@ class RoomListSearchPresenter @Inject constructor( } } - val searchResults by roomList - .rememberMappedSummaries() - .collectAsState(initial = persistentListOf()) + val searchResults by dataSource.roomSummaries.collectAsState(initial = persistentListOf()) return RoomListSearchState( isSearchActive = isSearchActive, @@ -103,16 +71,4 @@ class RoomListSearchPresenter @Inject constructor( 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/test/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersPresenterTests.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersPresenterTests.kt index 995c02762a..15ee711e82 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersPresenterTests.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersPresenterTests.kt @@ -69,7 +69,6 @@ class RoomListFiltersPresenterTests { ) val roomListCurrentFilter = roomListService.allRooms.currentFilter.value as MatrixRoomListFilter.All assertThat(roomListCurrentFilter.filters).containsExactly( - MatrixRoomListFilter.NonLeft, MatrixRoomListFilter.Category.Group, ) @@ -86,9 +85,7 @@ class RoomListFiltersPresenterTests { RoomListFilter.Favourites, ) val roomListCurrentFilter = roomListService.allRooms.currentFilter.value as MatrixRoomListFilter.All - assertThat(roomListCurrentFilter.filters).containsExactly( - MatrixRoomListFilter.NonLeft, - ) + assertThat(roomListCurrentFilter.filters).isEmpty() } } } 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 index fb124356e1..d3fc434f25 100644 --- 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 @@ -79,9 +79,7 @@ class RoomListSearchPresenterTests { assertThat( roomListService.allRooms.currentFilter.value ).isEqualTo( - RoomListFilter.all( - RoomListFilter.None, - ) + RoomListFilter.None ) state.eventSink(RoomListSearchEvents.QueryChanged("Search")) } @@ -90,10 +88,7 @@ class RoomListSearchPresenterTests { assertThat( roomListService.allRooms.currentFilter.value ).isEqualTo( - RoomListFilter.all( - RoomListFilter.NonLeft, - RoomListFilter.NormalizedMatchRoomName("Search") - ) + RoomListFilter.NormalizedMatchRoomName("Search") ) state.eventSink(RoomListSearchEvents.ClearQuery) } @@ -102,9 +97,7 @@ class RoomListSearchPresenterTests { assertThat( roomListService.allRooms.currentFilter.value ).isEqualTo( - RoomListFilter.all( - RoomListFilter.None, - ) + RoomListFilter.None ) } } @@ -141,11 +134,13 @@ fun TestScope.createRoomListSearchPresenter( roomListService: RoomListService = FakeRoomListService(), ): RoomListSearchPresenter { return RoomListSearchPresenter( - roomListService = roomListService, - roomSummaryFactory = RoomListRoomSummaryFactory( - lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter(), - roomLastMessageFormatter = FakeRoomLastMessageFormatter(), + dataSource = RoomListSearchDataSource( + roomListService = roomListService, + roomSummaryFactory = RoomListRoomSummaryFactory( + lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter(), + roomLastMessageFormatter = FakeRoomLastMessageFormatter(), ), - coroutineDispatchers = testCoroutineDispatchers(), + coroutineDispatchers = testCoroutineDispatchers(), + ) ) } 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 4ea79f31bb..b2262706b0 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 @@ -47,11 +47,6 @@ sealed interface RoomListFilter { 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. */ @@ -81,11 +76,4 @@ sealed interface RoomListFilter { 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 04018b7bea..5c526870e5 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,7 +17,6 @@ package io.element.android.libraries.matrix.api.roomlist import androidx.compose.runtime.Immutable -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.StateFlow /** @@ -42,13 +41,11 @@ interface RoomListService { /** * 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, 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 586dafba4c..4231af07d2 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,11 +23,13 @@ import io.element.android.libraries.matrix.api.roomlist.RoomSummary import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.getAndUpdate 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.RoomListService import kotlin.coroutines.CoroutineContext @@ -50,6 +52,7 @@ internal class RoomListFactory( innerProvider: suspend () -> InnerRoomList ): DynamicRoomList { val loadingStateFlow: MutableStateFlow = MutableStateFlow(RoomList.LoadingState.NotLoaded) + val filteredSummariesFlow = MutableSharedFlow>(replay = 1, extraBufferCapacity = 1) val summariesFlow = MutableSharedFlow>(replay = 1, extraBufferCapacity = 1) val processor = RoomSummaryListProcessor(summariesFlow, innerRoomListService, coroutineContext, roomSummaryDetailsFactory) // Makes sure we don't miss any events @@ -63,8 +66,8 @@ internal class RoomListFactory( innerRoomList?.let { innerRoomList -> innerRoomList.entriesFlow( pageSize = pageSize, - initialFilterKind = initialFilter.toRustFilter(), - roomListDynamicEvents = dynamicEvents + roomListDynamicEvents = dynamicEvents, + initialFilterKind = RoomListEntriesDynamicFilterKind.NonLeft ).onEach { update -> processor.postUpdate(update) }.launchIn(this) @@ -75,12 +78,21 @@ internal class RoomListFactory( loadingStateFlow.value = it } .launchIn(this) + + combine( + currentFilter, + summariesFlow + ) { filter, summaries -> + summaries.filter(filter) + }.onEach { + filteredSummariesFlow.emit(it) + }.launchIn(this) } }.invokeOnCompletion { innerRoomList?.destroy() } return RustDynamicRoomList( - summaries = summariesFlow, + summaries = filteredSummariesFlow, loadingState = loadingStateFlow, currentFilter = currentFilter, loadedPages = loadedPages, @@ -106,8 +118,6 @@ private class RustDynamicRoomList( override suspend fun updateFilter(filter: RoomListFilter) { currentFilter.emit(filter) - val filterEvent = RoomListDynamicEvents.SetFilter(filter.toRustFilter()) - dynamicEvents.emit(filterEvent) } override suspend fun loadMore() { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilter.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilter.kt index d72ff9fbeb..f7a9e77509 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilter.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilter.kt @@ -17,20 +17,41 @@ package io.element.android.libraries.matrix.impl.roomlist import io.element.android.libraries.matrix.api.roomlist.RoomListFilter -import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind -import org.matrix.rustcomponents.sdk.RoomListFilterCategory +import io.element.android.libraries.matrix.api.roomlist.RoomSummary -fun RoomListFilter.toRustFilter(): RoomListEntriesDynamicFilterKind { - return when (this) { - is RoomListFilter.All -> RoomListEntriesDynamicFilterKind.All(filters.map { it.toRustFilter() }) - is RoomListFilter.Any -> RoomListEntriesDynamicFilterKind.Any(filters.map { it.toRustFilter() }) - RoomListFilter.Category.Group -> RoomListEntriesDynamicFilterKind.Category(RoomListFilterCategory.GROUP) - RoomListFilter.Category.People -> RoomListEntriesDynamicFilterKind.Category(RoomListFilterCategory.PEOPLE) - is RoomListFilter.FuzzyMatchRoomName -> RoomListEntriesDynamicFilterKind.FuzzyMatchRoomName(pattern) - RoomListFilter.NonLeft -> RoomListEntriesDynamicFilterKind.NonLeft - RoomListFilter.None -> RoomListEntriesDynamicFilterKind.None - is RoomListFilter.NormalizedMatchRoomName -> RoomListEntriesDynamicFilterKind.NormalizedMatchRoomName(pattern) - RoomListFilter.Unread -> RoomListEntriesDynamicFilterKind.Unread - RoomListFilter.Favorite -> RoomListEntriesDynamicFilterKind.Favourite +val RoomListFilter.predicate + get() = when (this) { + is RoomListFilter.All -> { _: RoomSummary -> true } + is RoomListFilter.Any -> { _: RoomSummary -> true } + RoomListFilter.None -> { _: RoomSummary -> false } + RoomListFilter.Category.Group -> { roomSummary: RoomSummary -> + roomSummary is RoomSummary.Filled && !roomSummary.details.isDirect + } + RoomListFilter.Category.People -> { roomSummary: RoomSummary -> + roomSummary is RoomSummary.Filled && roomSummary.details.isDirect + } + RoomListFilter.Favorite -> { roomSummary: RoomSummary -> + roomSummary is RoomSummary.Filled && roomSummary.details.isFavorite + } + RoomListFilter.Unread -> { roomSummary: RoomSummary -> + roomSummary is RoomSummary.Filled && + (roomSummary.details.numUnreadNotifications > 0 || roomSummary.details.isMarkedUnread) + } + is RoomListFilter.NormalizedMatchRoomName -> { roomSummary: RoomSummary -> + roomSummary is RoomSummary.Filled && roomSummary.details.name.contains(pattern, ignoreCase = true) + } + } + +fun List.filter(filter: RoomListFilter): List { + return when (filter) { + is RoomListFilter.All -> { + val predicates = filter.filters.map { it.predicate } + filter { roomSummary -> predicates.all { it(roomSummary) } } + } + is RoomListFilter.Any -> { + val predicates = filter.filters.map { it.predicate } + filter { roomSummary -> predicates.any { it(roomSummary) } } + } + else -> filter(filter.predicate) } } 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 413c38be6e..70310e472e 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 @@ -47,7 +47,6 @@ internal class RustRoomListService( private val roomListFactory: RoomListFactory, ) : RoomListService { override fun createRoomList( - coroutineScope: CoroutineScope, pageSize: Int, initialFilter: RoomListFilter, source: RoomList.Source @@ -55,7 +54,6 @@ internal class RustRoomListService( return roomListFactory.createRoomList( pageSize = pageSize, initialFilter = initialFilter, - coroutineScope = coroutineScope, coroutineContext = sessionDispatcher, ) { when (source) { @@ -68,7 +66,6 @@ internal class RustRoomListService( override val allRooms: DynamicRoomList = roomListFactory.createRoomList( pageSize = DEFAULT_PAGE_SIZE, coroutineContext = sessionDispatcher, - initialFilter = RoomListFilter.all(RoomListFilter.NonLeft), ) { innerRoomListService.allRooms() } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilterTests.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilterTests.kt new file mode 100644 index 0000000000..0e778ac29a --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilterTests.kt @@ -0,0 +1,126 @@ +/* + * 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.libraries.matrix.impl.roomlist + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.roomlist.RoomListFilter +import io.element.android.libraries.matrix.test.room.aRoomSummaryDetails +import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class RoomListFilterTests { + private val regularRoom = aRoomSummaryFilled( + aRoomSummaryDetails( + isDirect = false + ) + ) + private val directRoom = aRoomSummaryFilled( + aRoomSummaryDetails( + isDirect = true + ) + ) + private val favoriteRoom = aRoomSummaryFilled( + aRoomSummaryDetails( + isFavorite = true + ) + ) + private val markedAsUnreadRoom = aRoomSummaryFilled( + aRoomSummaryDetails( + isMarkedUnread = true + ) + ) + private val unreadNotificationRoom = aRoomSummaryFilled( + aRoomSummaryDetails( + numUnreadNotifications = 1 + ) + ) + private val roomToSearch = aRoomSummaryFilled( + aRoomSummaryDetails( + name = "Room to search" + ) + ) + + private val roomSummaries = listOf( + regularRoom, + directRoom, + favoriteRoom, + markedAsUnreadRoom, + unreadNotificationRoom, + roomToSearch + ) + + @Test + fun `Room list filter all empty`() = runTest { + val filter = RoomListFilter.all() + assertThat(roomSummaries.filter(filter)).isEqualTo(roomSummaries) + } + + @Test + fun `Room list filter none`() = runTest { + val filter = RoomListFilter.None + assertThat(roomSummaries.filter(filter)).isEmpty() + } + + @Test + fun `Room list filter people`() = runTest { + val filter = RoomListFilter.Category.People + assertThat(roomSummaries.filter(filter)).containsExactly(directRoom) + } + + @Test + fun `Room list filter group`() = runTest { + val filter = RoomListFilter.Category.Group + assertThat(roomSummaries.filter(filter)).containsExactly(regularRoom, favoriteRoom, markedAsUnreadRoom, unreadNotificationRoom, roomToSearch) + } + + @Test + fun `Room list filter favorite`() = runTest { + val filter = RoomListFilter.Favorite + assertThat(roomSummaries.filter(filter)).containsExactly(favoriteRoom) + } + + @Test + fun `Room list filter unread`() = runTest { + val filter = RoomListFilter.Unread + assertThat(roomSummaries.filter(filter)).containsExactly(markedAsUnreadRoom, unreadNotificationRoom) + } + + @Test + fun `Room list filter normalized match room name`() = runTest { + val filter = RoomListFilter.NormalizedMatchRoomName("search") + assertThat(roomSummaries.filter(filter)).containsExactly(roomToSearch) + } + + @Test + fun `Room list filter all with one match`() = runTest { + val filter = RoomListFilter.all( + RoomListFilter.Category.Group, + RoomListFilter.Favorite + ) + assertThat(roomSummaries.filter(filter)).containsExactly(favoriteRoom) + } + + @Test + fun `Room list filter all with no match`() = runTest { + val filter = RoomListFilter.all( + RoomListFilter.Category.People, + RoomListFilter.Favorite + ) + assertThat(roomSummaries.filter(filter)).isEmpty() + } +} 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 d53596e462..5de62272d3 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,7 +21,6 @@ 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 @@ -60,7 +59,11 @@ class FakeRoomListService : RoomListService { var latestSlidingSyncRange: IntRange? = null private set - override fun createRoomList(coroutineScope: CoroutineScope, pageSize: Int, initialFilter: RoomListFilter, source: RoomList.Source): DynamicRoomList { + override fun createRoomList( + pageSize: Int, + initialFilter: RoomListFilter, + source: RoomList.Source + ): DynamicRoomList { return when (source) { RoomList.Source.All -> allRooms RoomList.Source.Invites -> invites diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt index 40245d2701..0b4c47800c 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt @@ -31,6 +31,7 @@ import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryF import io.element.android.features.roomlist.impl.filters.RoomListFiltersPresenter import io.element.android.features.roomlist.impl.migration.MigrationScreenPresenter import io.element.android.features.roomlist.impl.migration.SharedPrefsMigrationScreenStore +import io.element.android.features.roomlist.impl.search.RoomListSearchDataSource import io.element.android.features.roomlist.impl.search.RoomListSearchPresenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.dateformatter.impl.DateFormatters @@ -112,9 +113,11 @@ class RoomListScreen( migrationScreenStore = SharedPrefsMigrationScreenStore(context.getSharedPreferences("migration", Context.MODE_PRIVATE)) ), searchPresenter = RoomListSearchPresenter( - roomListService = matrixClient.roomListService, - roomSummaryFactory = roomListRoomSummaryFactory, - coroutineDispatchers = coroutineDispatchers, + RoomListSearchDataSource( + roomListService = matrixClient.roomListService, + roomSummaryFactory = roomListRoomSummaryFactory, + coroutineDispatchers = coroutineDispatchers, + ) ), sessionPreferencesStore = DefaultSessionPreferencesStore( context = context,