From 417492683d508ba304853d013a3fe30a82b03568 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 30 May 2024 17:31:15 +0200 Subject: [PATCH] Ensure that room where is user is invited are not listed when forwarding a message. --- .../roomselect/impl/RoomSelectPresenter.kt | 44 ++++++------ .../impl/RoomSelectSearchDataSource.kt | 70 +++++++++++++++++++ .../impl/RoomSelectPresenterTest.kt | 51 +++++++++----- 3 files changed, 125 insertions(+), 40 deletions(-) create mode 100644 libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectSearchDataSource.kt diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt index 77eb7b9845..e56d4dce09 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt @@ -19,6 +19,7 @@ package io.element.android.libraries.roomselect.impl import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -28,17 +29,14 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.designsystem.theme.components.SearchBarResultState -import io.element.android.libraries.matrix.api.MatrixClient -import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails import io.element.android.libraries.roomselect.api.RoomSelectMode -import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toPersistentList +import kotlinx.collections.immutable.toImmutableList class RoomSelectPresenter @AssistedInject constructor( @Assisted private val mode: RoomSelectMode, - private val client: MatrixClient, + private val dataSource: RoomSelectSearchDataSource, ) : Presenter { @AssistedFactory interface Factory { @@ -48,22 +46,26 @@ class RoomSelectPresenter @AssistedInject constructor( @Composable override fun present(): RoomSelectState { var selectedRooms by remember { mutableStateOf(persistentListOf()) } - var query by remember { mutableStateOf("") } + var searchQuery by remember { mutableStateOf("") } var isSearchActive by remember { mutableStateOf(false) } - var results: SearchBarResultState> by remember { mutableStateOf(SearchBarResultState.Initial()) } - val summaries by client.roomListService.allRooms.summaries.collectAsState(initial = emptyList()) + LaunchedEffect(Unit) { + dataSource.load() + } - LaunchedEffect(query, summaries) { - val filteredSummaries = summaries.filterIsInstance() - .map { it.details } - .filter { it.name.orEmpty().contains(query, ignoreCase = true) } - .distinctBy { it.roomId } // This should be removed once we're sure no duplicate Rooms can be received - .toPersistentList() - results = if (filteredSummaries.isNotEmpty()) { - SearchBarResultState.Results(filteredSummaries) - } else { - SearchBarResultState.NoResultsFound() + LaunchedEffect(searchQuery) { + dataSource.setSearchQuery(searchQuery) + } + + val roomSummaryDetailsList by dataSource.roomSummaries.collectAsState(initial = persistentListOf()) + + val searchResults by remember { + derivedStateOf { + when { + roomSummaryDetailsList.isNotEmpty() -> SearchBarResultState.Results(roomSummaryDetailsList.toImmutableList()) + isSearchActive -> SearchBarResultState.NoResultsFound() + else -> SearchBarResultState.Initial() + } } } @@ -80,15 +82,15 @@ class RoomSelectPresenter @AssistedInject constructor( // } } RoomSelectEvents.RemoveSelectedRoom -> selectedRooms = persistentListOf() - is RoomSelectEvents.UpdateQuery -> query = event.query + is RoomSelectEvents.UpdateQuery -> searchQuery = event.query RoomSelectEvents.ToggleSearchActive -> isSearchActive = !isSearchActive } } return RoomSelectState( mode = mode, - resultState = results, - query = query, + resultState = searchResults, + query = searchQuery, isSearchActive = isSearchActive, selectedRooms = selectedRooms, eventSink = { handleEvents(it) } diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectSearchDataSource.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectSearchDataSource.kt new file mode 100644 index 0000000000..a6705e65ba --- /dev/null +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectSearchDataSource.kt @@ -0,0 +1,70 @@ +/* + * 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.roomselect.impl + +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.matrix.api.room.CurrentUserMembership +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.RoomSummaryDetails +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 RoomSelectSearchDataSource @Inject constructor( + roomListService: RoomListService, + coroutineDispatchers: CoroutineDispatchers, +) { + private val roomList = roomListService.createRoomList( + pageSize = PAGE_SIZE, + initialFilter = RoomListFilter.all(), + source = RoomList.Source.All, + ) + + val roomSummaries: Flow> = roomList.filteredSummaries + .map { roomSummaries -> + roomSummaries + .filterIsInstance() + .map { it.details } + .filter { it.currentUserMembership == CurrentUserMembership.JOINED } + .distinctBy { it.roomId } // This should be removed once we're sure no duplicate Rooms can be received + .toPersistentList() + } + .flowOn(coroutineDispatchers.computation) + + suspend fun load() = coroutineScope { + roomList.loadAllIncrementally(this) + } + + suspend fun setSearchQuery(searchQuery: String) = coroutineScope { + val filter = if (searchQuery.isBlank()) { + RoomListFilter.all() + } else { + RoomListFilter.NormalizedMatchRoomName(searchQuery) + } + roomList.updateFilter(filter) + } +} diff --git a/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt b/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt index 3bc08b483f..b4117d3d40 100644 --- a/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt +++ b/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt @@ -21,13 +21,16 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +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.FakeMatrixClient import io.element.android.libraries.matrix.test.room.aRoomSummaryDetails import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService import io.element.android.libraries.roomselect.api.RoomSelectMode import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -38,7 +41,7 @@ class RoomSelectPresenterTest { @Test fun `present - initial state`() = runTest { - val presenter = aPresenter() + val presenter = createRoomSelectPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -46,24 +49,18 @@ class RoomSelectPresenterTest { assertThat(initialState.selectedRooms).isEmpty() assertThat(initialState.resultState).isInstanceOf(SearchBarResultState.Initial::class.java) assertThat(initialState.isSearchActive).isFalse() - // Search is run automatically - val searchState = awaitItem() - assertThat(searchState.resultState).isInstanceOf(SearchBarResultState.NoResultsFound::class.java) } } @Test fun `present - toggle search active`() = runTest { - val presenter = aPresenter() + val presenter = createRoomSelectPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initialState = awaitItem() - skipItems(1) - initialState.eventSink(RoomSelectEvents.ToggleSearchActive) assertThat(awaitItem().isSearchActive).isTrue() - initialState.eventSink(RoomSelectEvents.ToggleSearchActive) assertThat(awaitItem().isSearchActive).isFalse() } @@ -74,43 +71,59 @@ class RoomSelectPresenterTest { val roomListService = FakeRoomListService().apply { postAllRooms(listOf(RoomSummary.Filled(aRoomSummaryDetails()))) } - val client = FakeMatrixClient(roomListService = roomListService) - val presenter = aPresenter(client = client) + val presenter = createRoomSelectPresenter( + roomListService = roomListService + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initialState = awaitItem() assertThat(awaitItem().resultState as? SearchBarResultState.Results).isEqualTo(SearchBarResultState.Results(listOf(aRoomSummaryDetails()))) - + initialState.eventSink(RoomSelectEvents.ToggleSearchActive) + skipItems(1) initialState.eventSink(RoomSelectEvents.UpdateQuery("string not contained")) + assertThat( + roomListService.allRooms.currentFilter.value + ).isEqualTo( + RoomListFilter.NormalizedMatchRoomName("string not contained") + ) assertThat(awaitItem().query).isEqualTo("string not contained") + roomListService.postAllRooms( + emptyList() + ) assertThat(awaitItem().resultState).isInstanceOf(SearchBarResultState.NoResultsFound::class.java) } } @Test fun `present - select and remove a room`() = runTest { - val presenter = aPresenter() + val roomListService = FakeRoomListService().apply { + postAllRooms(listOf(RoomSummary.Filled(aRoomSummaryDetails()))) + } + val presenter = createRoomSelectPresenter( + roomListService = roomListService, + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initialState = awaitItem() - skipItems(1) val summary = aRoomSummaryDetails() - initialState.eventSink(RoomSelectEvents.SetSelectedRoom(summary)) assertThat(awaitItem().selectedRooms).isEqualTo(persistentListOf(summary)) - initialState.eventSink(RoomSelectEvents.RemoveSelectedRoom) assertThat(awaitItem().selectedRooms).isEmpty() + cancel() } } - private fun aPresenter( + private fun TestScope.createRoomSelectPresenter( mode: RoomSelectMode = RoomSelectMode.Forward, - client: FakeMatrixClient = FakeMatrixClient(), + roomListService: RoomListService = FakeRoomListService(), ) = RoomSelectPresenter( mode = mode, - client = client, + dataSource = RoomSelectSearchDataSource( + roomListService = roomListService, + coroutineDispatchers = testCoroutineDispatchers(), + ), ) }