Ensure that room where is user is invited are not listed when forwarding a message.
This commit is contained in:
parent
ff697f68fc
commit
417492683d
3 changed files with 125 additions and 40 deletions
|
|
@ -19,6 +19,7 @@ package io.element.android.libraries.roomselect.impl
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
|
@ -28,17 +29,14 @@ import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
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.matrix.api.roomlist.RoomSummaryDetails
|
||||||
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
class RoomSelectPresenter @AssistedInject constructor(
|
class RoomSelectPresenter @AssistedInject constructor(
|
||||||
@Assisted private val mode: RoomSelectMode,
|
@Assisted private val mode: RoomSelectMode,
|
||||||
private val client: MatrixClient,
|
private val dataSource: RoomSelectSearchDataSource,
|
||||||
) : Presenter<RoomSelectState> {
|
) : Presenter<RoomSelectState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
|
@ -48,22 +46,26 @@ class RoomSelectPresenter @AssistedInject constructor(
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): RoomSelectState {
|
override fun present(): RoomSelectState {
|
||||||
var selectedRooms by remember { mutableStateOf(persistentListOf<RoomSummaryDetails>()) }
|
var selectedRooms by remember { mutableStateOf(persistentListOf<RoomSummaryDetails>()) }
|
||||||
var query by remember { mutableStateOf("") }
|
var searchQuery by remember { mutableStateOf("") }
|
||||||
var isSearchActive by remember { mutableStateOf(false) }
|
var isSearchActive by remember { mutableStateOf(false) }
|
||||||
var results: SearchBarResultState<ImmutableList<RoomSummaryDetails>> by remember { mutableStateOf(SearchBarResultState.Initial()) }
|
|
||||||
|
|
||||||
val summaries by client.roomListService.allRooms.summaries.collectAsState(initial = emptyList())
|
LaunchedEffect(Unit) {
|
||||||
|
dataSource.load()
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(query, summaries) {
|
LaunchedEffect(searchQuery) {
|
||||||
val filteredSummaries = summaries.filterIsInstance<RoomSummary.Filled>()
|
dataSource.setSearchQuery(searchQuery)
|
||||||
.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
|
val roomSummaryDetailsList by dataSource.roomSummaries.collectAsState(initial = persistentListOf())
|
||||||
.toPersistentList()
|
|
||||||
results = if (filteredSummaries.isNotEmpty()) {
|
val searchResults by remember {
|
||||||
SearchBarResultState.Results(filteredSummaries)
|
derivedStateOf {
|
||||||
} else {
|
when {
|
||||||
SearchBarResultState.NoResultsFound()
|
roomSummaryDetailsList.isNotEmpty() -> SearchBarResultState.Results(roomSummaryDetailsList.toImmutableList())
|
||||||
|
isSearchActive -> SearchBarResultState.NoResultsFound()
|
||||||
|
else -> SearchBarResultState.Initial()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -80,15 +82,15 @@ class RoomSelectPresenter @AssistedInject constructor(
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
RoomSelectEvents.RemoveSelectedRoom -> selectedRooms = persistentListOf()
|
RoomSelectEvents.RemoveSelectedRoom -> selectedRooms = persistentListOf()
|
||||||
is RoomSelectEvents.UpdateQuery -> query = event.query
|
is RoomSelectEvents.UpdateQuery -> searchQuery = event.query
|
||||||
RoomSelectEvents.ToggleSearchActive -> isSearchActive = !isSearchActive
|
RoomSelectEvents.ToggleSearchActive -> isSearchActive = !isSearchActive
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return RoomSelectState(
|
return RoomSelectState(
|
||||||
mode = mode,
|
mode = mode,
|
||||||
resultState = results,
|
resultState = searchResults,
|
||||||
query = query,
|
query = searchQuery,
|
||||||
isSearchActive = isSearchActive,
|
isSearchActive = isSearchActive,
|
||||||
selectedRooms = selectedRooms,
|
selectedRooms = selectedRooms,
|
||||||
eventSink = { handleEvents(it) }
|
eventSink = { handleEvents(it) }
|
||||||
|
|
|
||||||
|
|
@ -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<PersistentList<RoomSummaryDetails>> = roomList.filteredSummaries
|
||||||
|
.map { roomSummaries ->
|
||||||
|
roomSummaries
|
||||||
|
.filterIsInstance<RoomSummary.Filled>()
|
||||||
|
.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,13 +21,16 @@ import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
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.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.room.aRoomSummaryDetails
|
||||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||||
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -38,7 +41,7 @@ class RoomSelectPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state`() = runTest {
|
fun `present - initial state`() = runTest {
|
||||||
val presenter = aPresenter()
|
val presenter = createRoomSelectPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -46,24 +49,18 @@ class RoomSelectPresenterTest {
|
||||||
assertThat(initialState.selectedRooms).isEmpty()
|
assertThat(initialState.selectedRooms).isEmpty()
|
||||||
assertThat(initialState.resultState).isInstanceOf(SearchBarResultState.Initial::class.java)
|
assertThat(initialState.resultState).isInstanceOf(SearchBarResultState.Initial::class.java)
|
||||||
assertThat(initialState.isSearchActive).isFalse()
|
assertThat(initialState.isSearchActive).isFalse()
|
||||||
// Search is run automatically
|
|
||||||
val searchState = awaitItem()
|
|
||||||
assertThat(searchState.resultState).isInstanceOf(SearchBarResultState.NoResultsFound::class.java)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - toggle search active`() = runTest {
|
fun `present - toggle search active`() = runTest {
|
||||||
val presenter = aPresenter()
|
val presenter = createRoomSelectPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
skipItems(1)
|
|
||||||
|
|
||||||
initialState.eventSink(RoomSelectEvents.ToggleSearchActive)
|
initialState.eventSink(RoomSelectEvents.ToggleSearchActive)
|
||||||
assertThat(awaitItem().isSearchActive).isTrue()
|
assertThat(awaitItem().isSearchActive).isTrue()
|
||||||
|
|
||||||
initialState.eventSink(RoomSelectEvents.ToggleSearchActive)
|
initialState.eventSink(RoomSelectEvents.ToggleSearchActive)
|
||||||
assertThat(awaitItem().isSearchActive).isFalse()
|
assertThat(awaitItem().isSearchActive).isFalse()
|
||||||
}
|
}
|
||||||
|
|
@ -74,43 +71,59 @@ class RoomSelectPresenterTest {
|
||||||
val roomListService = FakeRoomListService().apply {
|
val roomListService = FakeRoomListService().apply {
|
||||||
postAllRooms(listOf(RoomSummary.Filled(aRoomSummaryDetails())))
|
postAllRooms(listOf(RoomSummary.Filled(aRoomSummaryDetails())))
|
||||||
}
|
}
|
||||||
val client = FakeMatrixClient(roomListService = roomListService)
|
val presenter = createRoomSelectPresenter(
|
||||||
val presenter = aPresenter(client = client)
|
roomListService = roomListService
|
||||||
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(awaitItem().resultState as? SearchBarResultState.Results).isEqualTo(SearchBarResultState.Results(listOf(aRoomSummaryDetails())))
|
assertThat(awaitItem().resultState as? SearchBarResultState.Results).isEqualTo(SearchBarResultState.Results(listOf(aRoomSummaryDetails())))
|
||||||
|
initialState.eventSink(RoomSelectEvents.ToggleSearchActive)
|
||||||
|
skipItems(1)
|
||||||
initialState.eventSink(RoomSelectEvents.UpdateQuery("string not contained"))
|
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")
|
assertThat(awaitItem().query).isEqualTo("string not contained")
|
||||||
|
roomListService.postAllRooms(
|
||||||
|
emptyList()
|
||||||
|
)
|
||||||
assertThat(awaitItem().resultState).isInstanceOf(SearchBarResultState.NoResultsFound::class.java)
|
assertThat(awaitItem().resultState).isInstanceOf(SearchBarResultState.NoResultsFound::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - select and remove a room`() = runTest {
|
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) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
skipItems(1)
|
|
||||||
val summary = aRoomSummaryDetails()
|
val summary = aRoomSummaryDetails()
|
||||||
|
|
||||||
initialState.eventSink(RoomSelectEvents.SetSelectedRoom(summary))
|
initialState.eventSink(RoomSelectEvents.SetSelectedRoom(summary))
|
||||||
assertThat(awaitItem().selectedRooms).isEqualTo(persistentListOf(summary))
|
assertThat(awaitItem().selectedRooms).isEqualTo(persistentListOf(summary))
|
||||||
|
|
||||||
initialState.eventSink(RoomSelectEvents.RemoveSelectedRoom)
|
initialState.eventSink(RoomSelectEvents.RemoveSelectedRoom)
|
||||||
assertThat(awaitItem().selectedRooms).isEmpty()
|
assertThat(awaitItem().selectedRooms).isEmpty()
|
||||||
|
cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun aPresenter(
|
private fun TestScope.createRoomSelectPresenter(
|
||||||
mode: RoomSelectMode = RoomSelectMode.Forward,
|
mode: RoomSelectMode = RoomSelectMode.Forward,
|
||||||
client: FakeMatrixClient = FakeMatrixClient(),
|
roomListService: RoomListService = FakeRoomListService(),
|
||||||
) = RoomSelectPresenter(
|
) = RoomSelectPresenter(
|
||||||
mode = mode,
|
mode = mode,
|
||||||
client = client,
|
dataSource = RoomSelectSearchDataSource(
|
||||||
|
roomListService = roomListService,
|
||||||
|
coroutineDispatchers = testCoroutineDispatchers(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue