mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-04 10:32:34 +00:00
Merge pull request #7883 from element-hq/feature/bma/roomSelectSearchField
Use a search field to filter the rooms in the room select screen
This commit is contained in:
+8
-4
@@ -57,7 +57,7 @@ fun RoomListInterface.loadingStateFlow(): Flow<RoomListLoadingState> =
|
|||||||
internal fun RoomListInterface.entriesFlow(
|
internal fun RoomListInterface.entriesFlow(
|
||||||
pageSize: Int,
|
pageSize: Int,
|
||||||
initialFilterKind: RoomListEntriesDynamicFilterKind,
|
initialFilterKind: RoomListEntriesDynamicFilterKind,
|
||||||
onControllerCreated: (RoomListDynamicEntriesController) -> Unit,
|
onControllerCreated: suspend (RoomListDynamicEntriesController) -> Unit,
|
||||||
): Flow<List<RoomListEntriesUpdate>> =
|
): Flow<List<RoomListEntriesUpdate>> =
|
||||||
callbackFlow {
|
callbackFlow {
|
||||||
val listener = object : RoomListEntriesListener {
|
val listener = object : RoomListEntriesListener {
|
||||||
@@ -70,9 +70,13 @@ internal fun RoomListInterface.entriesFlow(
|
|||||||
listener = listener,
|
listener = listener,
|
||||||
)
|
)
|
||||||
val controller = result.controller()
|
val controller = result.controller()
|
||||||
controller.setFilter(initialFilterKind)
|
// Use try/finally rather than awaitClose's block, so that the resources are released even if
|
||||||
onControllerCreated(controller)
|
// onControllerCreated is cancelled or throws before awaitClose is reached
|
||||||
awaitClose {
|
try {
|
||||||
|
controller.setFilter(initialFilterKind)
|
||||||
|
onControllerCreated(controller)
|
||||||
|
awaitClose()
|
||||||
|
} finally {
|
||||||
result.entriesStream().cancelAndDestroy()
|
result.entriesStream().cancelAndDestroy()
|
||||||
controller.destroy()
|
controller.destroy()
|
||||||
result.destroy()
|
result.destroy()
|
||||||
|
|||||||
+9
-10
@@ -23,7 +23,6 @@ import kotlinx.coroutines.flow.launchIn
|
|||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.matrix.rustcomponents.sdk.RoomListDynamicEntriesController
|
|
||||||
import org.matrix.rustcomponents.sdk.RoomListLoadingState
|
import org.matrix.rustcomponents.sdk.RoomListLoadingState
|
||||||
import org.matrix.rustcomponents.sdk.RoomListService
|
import org.matrix.rustcomponents.sdk.RoomListService
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
@@ -49,7 +48,13 @@ internal class RoomListFactory(
|
|||||||
val summariesFlow = MutableSharedFlow<List<RoomSummary>>(replay = 1, extraBufferCapacity = 1)
|
val summariesFlow = MutableSharedFlow<List<RoomSummary>>(replay = 1, extraBufferCapacity = 1)
|
||||||
val processor = RoomSummaryListProcessor(summariesFlow, innerRoomListService, coroutineContext, roomSummaryFactory, analyticsService)
|
val processor = RoomSummaryListProcessor(summariesFlow, innerRoomListService, coroutineContext, roomSummaryFactory, analyticsService)
|
||||||
var innerRoomList: InnerRoomList? = null
|
var innerRoomList: InnerRoomList? = null
|
||||||
var dynamicController: RoomListDynamicEntriesController? = null
|
val dynamicRoomList = RustDynamicRoomList(
|
||||||
|
summaries = summariesFlow,
|
||||||
|
loadingState = loadingStateFlow,
|
||||||
|
processor = processor,
|
||||||
|
pageSize = pageSize,
|
||||||
|
initialFilter = initialFilter,
|
||||||
|
)
|
||||||
|
|
||||||
val firstRoomsTransaction = analyticsService.startTransaction("Load first set of rooms", "innerRoomList.entriesFlow")
|
val firstRoomsTransaction = analyticsService.startTransaction("Load first set of rooms", "innerRoomList.entriesFlow")
|
||||||
|
|
||||||
@@ -60,7 +65,7 @@ internal class RoomListFactory(
|
|||||||
pageSize = pageSize,
|
pageSize = pageSize,
|
||||||
initialFilterKind = RoomListFilterMapper.toRustFilter(initialFilter),
|
initialFilterKind = RoomListFilterMapper.toRustFilter(initialFilter),
|
||||||
onControllerCreated = { controller ->
|
onControllerCreated = { controller ->
|
||||||
dynamicController = controller
|
dynamicRoomList.onControllerCreated(controller, initialFilter)
|
||||||
}
|
}
|
||||||
).onEach { update ->
|
).onEach { update ->
|
||||||
if (!firstRoomsTransaction.isFinished()) {
|
if (!firstRoomsTransaction.isFinished()) {
|
||||||
@@ -80,13 +85,7 @@ internal class RoomListFactory(
|
|||||||
}.invokeOnCompletion {
|
}.invokeOnCompletion {
|
||||||
innerRoomList?.destroy()
|
innerRoomList?.destroy()
|
||||||
}
|
}
|
||||||
return RustDynamicRoomList(
|
return dynamicRoomList
|
||||||
summaries = summariesFlow,
|
|
||||||
loadingState = loadingStateFlow,
|
|
||||||
processor = processor,
|
|
||||||
pageSize = pageSize,
|
|
||||||
dynamicController = { dynamicController }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+32
-11
@@ -24,37 +24,58 @@ internal class RustDynamicRoomList(
|
|||||||
override val loadingState: MutableStateFlow<RoomList.LoadingState>,
|
override val loadingState: MutableStateFlow<RoomList.LoadingState>,
|
||||||
private val processor: RoomSummaryListProcessor,
|
private val processor: RoomSummaryListProcessor,
|
||||||
override val pageSize: Int,
|
override val pageSize: Int,
|
||||||
private val dynamicController: () -> RoomListDynamicEntriesController?,
|
initialFilter: RoomListFilter,
|
||||||
private val addPagesCount: Int = DEFAULT_ADD_PAGES_COUNT
|
private val addPagesCount: Int = DEFAULT_ADD_PAGES_COUNT
|
||||||
) : DynamicRoomList {
|
) : DynamicRoomList {
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
|
|
||||||
|
// The controller is created asynchronously, once the entries of the room list are observed
|
||||||
|
private var dynamicController: RoomListDynamicEntriesController? = null
|
||||||
|
|
||||||
|
// The last requested filter, so that it can be applied when the controller is created
|
||||||
|
private var currentFilter: RoomListFilter = initialFilter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To be called when the controller is created. The controller already has the initial filter set, so apply the filter
|
||||||
|
* only if it has been updated in the meantime.
|
||||||
|
*/
|
||||||
|
suspend fun onControllerCreated(controller: RoomListDynamicEntriesController, initialFilter: RoomListFilter) {
|
||||||
|
mutex.withLock {
|
||||||
|
dynamicController = controller
|
||||||
|
if (currentFilter != initialFilter) {
|
||||||
|
controller.applyFilter(currentFilter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun rebuildSummaries() {
|
override suspend fun rebuildSummaries() {
|
||||||
processor.rebuildRoomSummaries()
|
processor.rebuildRoomSummaries()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun updateFilter(filter: RoomListFilter) {
|
override suspend fun updateFilter(filter: RoomListFilter) {
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
dynamicController()?.let { controller ->
|
currentFilter = filter
|
||||||
// Reset pagination when filter changes
|
dynamicController?.applyFilter(filter)
|
||||||
controller.resetToOnePage()
|
|
||||||
val rustFilter = RoomListFilterMapper.toRustFilter(filter)
|
|
||||||
controller.setFilter(rustFilter)
|
|
||||||
// Then preload some pages
|
|
||||||
controller.addPages(addPagesCount)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun RoomListDynamicEntriesController.applyFilter(filter: RoomListFilter) {
|
||||||
|
// Reset pagination when filter changes
|
||||||
|
resetToOnePage()
|
||||||
|
setFilter(RoomListFilterMapper.toRustFilter(filter))
|
||||||
|
// Then preload some pages
|
||||||
|
addPages(addPagesCount)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun loadMore() {
|
override suspend fun loadMore() {
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
dynamicController()?.addPages(addPagesCount)
|
dynamicController?.addPages(addPagesCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun reset() {
|
override suspend fun reset() {
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
dynamicController()?.resetToOnePage()
|
dynamicController?.resetToOnePage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.matrix.impl.fixtures.fakes
|
||||||
|
|
||||||
|
import org.matrix.rustcomponents.sdk.NoHandle
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomListDynamicEntriesController
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind
|
||||||
|
|
||||||
|
class FakeFfiRoomListDynamicEntriesController : RoomListDynamicEntriesController(NoHandle) {
|
||||||
|
val filters = mutableListOf<RoomListEntriesDynamicFilterKind>()
|
||||||
|
|
||||||
|
override fun setFilter(kind: RoomListEntriesDynamicFilterKind): Boolean {
|
||||||
|
filters.add(kind)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resetToOnePage() = Unit
|
||||||
|
|
||||||
|
override fun addOnePage() = Unit
|
||||||
|
}
|
||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.matrix.impl.roomlist
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.RoomList
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||||
|
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoomListDynamicEntriesController
|
||||||
|
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoomListService
|
||||||
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class RustDynamicRoomListTest {
|
||||||
|
@Test
|
||||||
|
fun `updateFilter before the controller is created applies the filter when it is created`() = runTest {
|
||||||
|
val sut = createRustDynamicRoomList()
|
||||||
|
sut.updateFilter(RoomListFilter.NormalizedMatchRoomName("query"))
|
||||||
|
val controller = FakeFfiRoomListDynamicEntriesController()
|
||||||
|
sut.onControllerCreated(controller, initialFilter = RoomListFilter.all())
|
||||||
|
assertThat(controller.filters).containsExactly(RoomListFilterMapper.toRustFilter(RoomListFilter.NormalizedMatchRoomName("query")))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when the filter is not updated, the controller filter is not changed when it is created`() = runTest {
|
||||||
|
val sut = createRustDynamicRoomList()
|
||||||
|
val controller = FakeFfiRoomListDynamicEntriesController()
|
||||||
|
sut.onControllerCreated(controller, initialFilter = RoomListFilter.all())
|
||||||
|
assertThat(controller.filters).isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `updateFilter after the controller is created applies the filter`() = runTest {
|
||||||
|
val sut = createRustDynamicRoomList()
|
||||||
|
val controller = FakeFfiRoomListDynamicEntriesController()
|
||||||
|
sut.onControllerCreated(controller, initialFilter = RoomListFilter.all())
|
||||||
|
sut.updateFilter(RoomListFilter.NormalizedMatchRoomName("query"))
|
||||||
|
assertThat(controller.filters).containsExactly(RoomListFilterMapper.toRustFilter(RoomListFilter.NormalizedMatchRoomName("query")))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TestScope.createRustDynamicRoomList(): RustDynamicRoomList {
|
||||||
|
val summaries = MutableSharedFlow<List<RoomSummary>>(replay = 1)
|
||||||
|
return RustDynamicRoomList(
|
||||||
|
summaries = summaries,
|
||||||
|
loadingState = MutableStateFlow(RoomList.LoadingState.NotLoaded),
|
||||||
|
processor = RoomSummaryListProcessor(
|
||||||
|
summaries,
|
||||||
|
FakeFfiRoomListService(),
|
||||||
|
coroutineContext = StandardTestDispatcher(testScheduler),
|
||||||
|
roomSummaryFactory = RoomSummaryFactory(),
|
||||||
|
analyticsService = FakeAnalyticsService(),
|
||||||
|
),
|
||||||
|
pageSize = 10,
|
||||||
|
initialFilter = RoomListFilter.all(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
-1
@@ -13,7 +13,6 @@ import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
|||||||
|
|
||||||
sealed interface RoomSelectEvent {
|
sealed interface RoomSelectEvent {
|
||||||
data class ToggleSelectedRoom(val room: SelectRoomInfo) : RoomSelectEvent
|
data class ToggleSelectedRoom(val room: SelectRoomInfo) : RoomSelectEvent
|
||||||
data object ToggleSearchActive : RoomSelectEvent
|
|
||||||
data class UpdateVisibleRange(val range: IntRange) : RoomSelectEvent
|
data class UpdateVisibleRange(val range: IntRange) : RoomSelectEvent
|
||||||
data object ToggleAccountListExpanded : RoomSelectEvent
|
data object ToggleAccountListExpanded : RoomSelectEvent
|
||||||
data class SelectAccount(val sessionId: SessionId) : RoomSelectEvent
|
data class SelectAccount(val sessionId: SessionId) : RoomSelectEvent
|
||||||
|
|||||||
+2
-7
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
package io.element.android.libraries.roomselect.impl
|
package io.element.android.libraries.roomselect.impl
|
||||||
|
|
||||||
import androidx.compose.foundation.text.input.clearText
|
|
||||||
import androidx.compose.foundation.text.input.rememberTextFieldState
|
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -63,7 +62,6 @@ class RoomSelectPresenter(
|
|||||||
override fun present(): RoomSelectState {
|
override fun present(): RoomSelectState {
|
||||||
var selectedRooms by remember { mutableStateOf(persistentListOf<SelectRoomInfo>()) }
|
var selectedRooms by remember { mutableStateOf(persistentListOf<SelectRoomInfo>()) }
|
||||||
val queryState = rememberTextFieldState()
|
val queryState = rememberTextFieldState()
|
||||||
var isSearchActive by remember { mutableStateOf(false) }
|
|
||||||
var isAccountListExpanded by remember { mutableStateOf(false) }
|
var isAccountListExpanded by remember { mutableStateOf(false) }
|
||||||
var selectedSessionId by remember { mutableStateOf(initialSessionId) }
|
var selectedSessionId by remember { mutableStateOf(initialSessionId) }
|
||||||
val sessions by remember { sessionStore.sessionsFlow() }.collectAsState(initial = emptyList())
|
val sessions by remember { sessionStore.sessionsFlow() }.collectAsState(initial = emptyList())
|
||||||
@@ -136,7 +134,7 @@ class RoomSelectPresenter(
|
|||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
when {
|
when {
|
||||||
roomSummaryDetailsList.isNotEmpty() -> SearchBarResultState.Results(roomSummaryDetailsList.toImmutableList())
|
roomSummaryDetailsList.isNotEmpty() -> SearchBarResultState.Results(roomSummaryDetailsList.toImmutableList())
|
||||||
isSearchActive -> SearchBarResultState.NoResultsFound
|
queryState.text.isNotBlank() -> SearchBarResultState.NoResultsFound
|
||||||
else -> SearchBarResultState.Initial
|
else -> SearchBarResultState.Initial
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,13 +150,11 @@ class RoomSelectPresenter(
|
|||||||
selectedRooms.adding(event.room)
|
selectedRooms.adding(event.room)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RoomSelectEvent.ToggleSearchActive -> isSearchActive = !isSearchActive
|
|
||||||
RoomSelectEvent.ToggleAccountListExpanded -> isAccountListExpanded = !isAccountListExpanded
|
RoomSelectEvent.ToggleAccountListExpanded -> isAccountListExpanded = !isAccountListExpanded
|
||||||
is RoomSelectEvent.SelectAccount -> {
|
is RoomSelectEvent.SelectAccount -> {
|
||||||
selectedSessionId = event.sessionId
|
selectedSessionId = event.sessionId
|
||||||
// The selected rooms and the search query are specific to the session
|
// The selected rooms are specific to the session. The search query is kept, and applied to the rooms of the new session.
|
||||||
selectedRooms = persistentListOf()
|
selectedRooms = persistentListOf()
|
||||||
queryState.clearText()
|
|
||||||
isAccountListExpanded = false
|
isAccountListExpanded = false
|
||||||
}
|
}
|
||||||
is RoomSelectEvent.UpdateVisibleRange -> coroutineScope.launch {
|
is RoomSelectEvent.UpdateVisibleRange -> coroutineScope.launch {
|
||||||
@@ -172,7 +168,6 @@ class RoomSelectPresenter(
|
|||||||
maxNumberOfRooms = maxNumberOfRooms,
|
maxNumberOfRooms = maxNumberOfRooms,
|
||||||
resultState = searchResults,
|
resultState = searchResults,
|
||||||
searchQuery = queryState,
|
searchQuery = queryState,
|
||||||
isSearchActive = isSearchActive,
|
|
||||||
selectedRooms = selectedRooms,
|
selectedRooms = selectedRooms,
|
||||||
selectedAccount = selectedAccount,
|
selectedAccount = selectedAccount,
|
||||||
hasRoomListError = hasRoomListError,
|
hasRoomListError = hasRoomListError,
|
||||||
|
|||||||
-1
@@ -20,7 +20,6 @@ data class RoomSelectState(
|
|||||||
val maxNumberOfRooms: Int,
|
val maxNumberOfRooms: Int,
|
||||||
val resultState: SearchBarResultState<ImmutableList<SelectRoomInfo>>,
|
val resultState: SearchBarResultState<ImmutableList<SelectRoomInfo>>,
|
||||||
val searchQuery: TextFieldState,
|
val searchQuery: TextFieldState,
|
||||||
val isSearchActive: Boolean,
|
|
||||||
val selectedRooms: ImmutableList<SelectRoomInfo>,
|
val selectedRooms: ImmutableList<SelectRoomInfo>,
|
||||||
val selectedAccount: MatrixUser,
|
val selectedAccount: MatrixUser,
|
||||||
/**
|
/**
|
||||||
|
|||||||
+1
-5
@@ -26,17 +26,15 @@ open class RoomSelectStatePreviewParam : PreviewParameterProvider<RoomSelectStat
|
|||||||
override val values: Sequence<RoomSelectState>
|
override val values: Sequence<RoomSelectState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aRoomSelectState(),
|
aRoomSelectState(),
|
||||||
aRoomSelectState(searchQuery = "Test", isSearchActive = true),
|
aRoomSelectState(searchQuery = "Test", resultState = SearchBarResultState.NoResultsFound),
|
||||||
aRoomSelectState(resultState = SearchBarResultState.Results(aRoomSelectRoomList())),
|
aRoomSelectState(resultState = SearchBarResultState.Results(aRoomSelectRoomList())),
|
||||||
aRoomSelectState(
|
aRoomSelectState(
|
||||||
resultState = SearchBarResultState.Results(aRoomSelectRoomList()),
|
resultState = SearchBarResultState.Results(aRoomSelectRoomList()),
|
||||||
searchQuery = "Test",
|
searchQuery = "Test",
|
||||||
isSearchActive = true,
|
|
||||||
),
|
),
|
||||||
aRoomSelectState(
|
aRoomSelectState(
|
||||||
resultState = SearchBarResultState.Results(aRoomSelectRoomList()),
|
resultState = SearchBarResultState.Results(aRoomSelectRoomList()),
|
||||||
searchQuery = "Test",
|
searchQuery = "Test",
|
||||||
isSearchActive = true,
|
|
||||||
selectedRooms = aRoomSelectRoomList().subList(0, 1),
|
selectedRooms = aRoomSelectRoomList().subList(0, 1),
|
||||||
),
|
),
|
||||||
aRoomSelectState(
|
aRoomSelectState(
|
||||||
@@ -76,7 +74,6 @@ internal fun aRoomSelectState(
|
|||||||
maxNumberOfRooms: Int = 10,
|
maxNumberOfRooms: Int = 10,
|
||||||
resultState: SearchBarResultState<ImmutableList<SelectRoomInfo>> = SearchBarResultState.Initial,
|
resultState: SearchBarResultState<ImmutableList<SelectRoomInfo>> = SearchBarResultState.Initial,
|
||||||
searchQuery: String = "",
|
searchQuery: String = "",
|
||||||
isSearchActive: Boolean = false,
|
|
||||||
selectedRooms: ImmutableList<SelectRoomInfo> = persistentListOf(),
|
selectedRooms: ImmutableList<SelectRoomInfo> = persistentListOf(),
|
||||||
selectedAccount: MatrixUser = MatrixUser(UserId("@alice:example.org")),
|
selectedAccount: MatrixUser = MatrixUser(UserId("@alice:example.org")),
|
||||||
hasRoomListError: Boolean = false,
|
hasRoomListError: Boolean = false,
|
||||||
@@ -88,7 +85,6 @@ internal fun aRoomSelectState(
|
|||||||
maxNumberOfRooms = maxNumberOfRooms,
|
maxNumberOfRooms = maxNumberOfRooms,
|
||||||
resultState = resultState,
|
resultState = resultState,
|
||||||
searchQuery = TextFieldState(initialText = searchQuery),
|
searchQuery = TextFieldState(initialText = searchQuery),
|
||||||
isSearchActive = isSearchActive,
|
|
||||||
selectedRooms = selectedRooms,
|
selectedRooms = selectedRooms,
|
||||||
selectedAccount = selectedAccount,
|
selectedAccount = selectedAccount,
|
||||||
hasRoomListError = hasRoomListError,
|
hasRoomListError = hasRoomListError,
|
||||||
|
|||||||
+98
-69
@@ -10,6 +10,7 @@ package io.element.android.libraries.roomselect.impl
|
|||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.animate
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
@@ -24,18 +25,23 @@ import androidx.compose.foundation.layout.WindowInsets
|
|||||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
import androidx.compose.foundation.layout.asPaddingValues
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.only
|
import androidx.compose.foundation.layout.only
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.safeDrawing
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.rememberTopAppBarState
|
import androidx.compose.material3.rememberTopAppBarState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.key
|
import androidx.compose.runtime.key
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -46,9 +52,11 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.rotate
|
import androidx.compose.ui.draw.rotate
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.heading
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.semantics.stateDescription
|
import androidx.compose.ui.semantics.stateDescription
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -70,8 +78,8 @@ import io.element.android.libraries.designsystem.theme.components.Icon
|
|||||||
import io.element.android.libraries.designsystem.theme.components.ListItem
|
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||||
import io.element.android.libraries.designsystem.theme.components.ListSectionHeader
|
import io.element.android.libraries.designsystem.theme.components.ListSectionHeader
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBar
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchField
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
@@ -123,10 +131,8 @@ fun RoomSelectView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var canHandleBack by remember { mutableStateOf(true) }
|
var canHandleBack by remember { mutableStateOf(true) }
|
||||||
fun onBackButton(state: RoomSelectState) {
|
fun onBackButton() {
|
||||||
if (state.isSearchActive) {
|
if (canHandleBack) {
|
||||||
state.eventSink(RoomSelectEvent.ToggleSearchActive)
|
|
||||||
} else if (canHandleBack) {
|
|
||||||
canHandleBack = false
|
canHandleBack = false
|
||||||
onDismiss()
|
onDismiss()
|
||||||
}
|
}
|
||||||
@@ -134,16 +140,34 @@ fun RoomSelectView(
|
|||||||
|
|
||||||
BackHandler(
|
BackHandler(
|
||||||
enabled = canHandleBack,
|
enabled = canHandleBack,
|
||||||
onBack = { onBackButton(state) }
|
onBack = ::onBackButton,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val searchQuery = state.searchQuery.text.toString()
|
||||||
// Use a new list state when the selected account changes, so that the list of rooms is scrolled to the top
|
// Use a new list state when the selected account changes, so that the list of rooms is scrolled to the top
|
||||||
val lazyListState = key(state.selectedAccount.userId) { rememberLazyListState() }
|
val lazyListState = key(state.selectedAccount.userId) { rememberLazyListState() }
|
||||||
|
// Scroll the list of rooms to the top when the search query changes
|
||||||
|
LaunchedEffect(lazyListState, searchQuery) {
|
||||||
|
lazyListState.scrollToItem(0)
|
||||||
|
}
|
||||||
// Hide the account switch section when the room list is scrolled up, and show it again as soon as it is scrolled down.
|
// Hide the account switch section when the room list is scrolled up, and show it again as soon as it is scrolled down.
|
||||||
// Use a new state when the selected account changes, so that the section is fully displayed again.
|
// Use a new state when the selected account changes, so that the section is fully displayed again.
|
||||||
val accountSwitchScrollBehavior = key(state.selectedAccount.userId) {
|
val accountSwitchScrollBehavior = key(state.selectedAccount.userId) {
|
||||||
TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState())
|
TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState())
|
||||||
}
|
}
|
||||||
|
// Collapse the account switch section when the user starts searching. It can be displayed again by scrolling down.
|
||||||
|
val isSearching = searchQuery.isNotBlank()
|
||||||
|
LaunchedEffect(isSearching) {
|
||||||
|
if (isSearching) {
|
||||||
|
val topAppBarState = accountSwitchScrollBehavior.state
|
||||||
|
animate(
|
||||||
|
initialValue = topAppBarState.heightOffset,
|
||||||
|
targetValue = topAppBarState.heightOffsetLimit,
|
||||||
|
) { value, _ ->
|
||||||
|
topAppBarState.heightOffset = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
OnVisibleRangeChangeEffect(lazyListState) { visibleRange ->
|
OnVisibleRangeChangeEffect(lazyListState) { visibleRange ->
|
||||||
state.eventSink(RoomSelectEvent.UpdateVisibleRange(visibleRange))
|
state.eventSink(RoomSelectEvent.UpdateVisibleRange(visibleRange))
|
||||||
}
|
}
|
||||||
@@ -159,7 +183,7 @@ fun RoomSelectView(
|
|||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
BackButton(
|
BackButton(
|
||||||
enabled = canHandleBack,
|
enabled = canHandleBack,
|
||||||
onClick = { onBackButton(state) }
|
onClick = ::onBackButton,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
@@ -179,89 +203,60 @@ fun RoomSelectView(
|
|||||||
.consumeWindowInsets(paddingValues)
|
.consumeWindowInsets(paddingValues)
|
||||||
.then(
|
.then(
|
||||||
// Only when the account switch section is displayed, else its unknown height would consume all the scroll events
|
// Only when the account switch section is displayed, else its unknown height would consume all the scroll events
|
||||||
if (state.showAccountSwitch && !state.isSearchActive) {
|
if (state.showAccountSwitch) {
|
||||||
Modifier.nestedScroll(accountSwitchScrollBehavior.nestedScrollConnection)
|
Modifier.nestedScroll(accountSwitchScrollBehavior.nestedScrollConnection)
|
||||||
} else {
|
} else {
|
||||||
Modifier
|
Modifier
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
SearchBar(
|
SearchField(
|
||||||
|
state = state.searchQuery,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(
|
.padding(
|
||||||
WindowInsets.safeDrawing
|
WindowInsets.safeDrawing
|
||||||
.only(WindowInsetsSides.Horizontal)
|
.only(WindowInsetsSides.Horizontal)
|
||||||
.asPaddingValues()
|
.asPaddingValues()
|
||||||
),
|
)
|
||||||
placeHolderTitle = stringResource(CommonStrings.action_search),
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
queryState = state.searchQuery,
|
placeholder = stringResource(CommonStrings.action_search),
|
||||||
active = state.isSearchActive,
|
)
|
||||||
onActiveChange = { state.eventSink(RoomSelectEvent.ToggleSearchActive) },
|
if (state.showAccountSwitch) {
|
||||||
resultState = state.resultState,
|
// Keep this space outside the collapsing section, so that the section does not touch the search field when it is collapsing
|
||||||
showBackButton = false,
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
) { summaries ->
|
TopAppBarScrollBehaviorLayout(scrollBehavior = accountSwitchScrollBehavior) {
|
||||||
LazyColumn(
|
AccountSwitchSection(
|
||||||
state = lazyListState,
|
selectedAccount = state.selectedAccount,
|
||||||
contentPadding = lazyColumnContentPadding,
|
otherAccounts = state.otherAccounts,
|
||||||
) {
|
isExpanded = state.isAccountListExpanded,
|
||||||
if (summaries.isNotEmpty()) {
|
onToggleExpand = { state.eventSink(RoomSelectEvent.ToggleAccountListExpanded) },
|
||||||
item {
|
onSelectAccount = { state.eventSink(RoomSelectEvent.SelectAccount(it)) },
|
||||||
ListSectionHeader(
|
)
|
||||||
title = stringResource(CommonStrings.common_header_rooms),
|
|
||||||
hasDivider = false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
item {
|
|
||||||
SelectedRoomsHelper(
|
|
||||||
selectedRooms = state.selectedRooms,
|
|
||||||
showVerticalSpace = false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
items(summaries, key = { it.roomId.value }) { roomSummary ->
|
|
||||||
RoomSummaryView(
|
|
||||||
roomSummary,
|
|
||||||
isSelected = state.selectedRooms.any { it.roomId == roomSummary.roomId },
|
|
||||||
onSelection = { roomSummary ->
|
|
||||||
state.eventSink(RoomSelectEvent.ToggleSelectedRoom(roomSummary))
|
|
||||||
},
|
|
||||||
canBeSelected = state.canSelectMoreRooms,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
SelectedRoomsHelper(
|
||||||
if (!state.isSearchActive) {
|
selectedRooms = state.selectedRooms,
|
||||||
if (state.showAccountSwitch) {
|
// showVerticalSpace only if there is no other accounts
|
||||||
// Keep this space outside the collapsing section, so that the section does not touch the search bar when it is collapsing
|
showVerticalSpace = !state.showAccountSwitch,
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
)
|
||||||
TopAppBarScrollBehaviorLayout(scrollBehavior = accountSwitchScrollBehavior) {
|
when {
|
||||||
AccountSwitchSection(
|
state.hasRoomListError -> RoomListError()
|
||||||
selectedAccount = state.selectedAccount,
|
state.resultState is SearchBarResultState.NoResultsFound -> NoResultsView(searchQuery = searchQuery)
|
||||||
otherAccounts = state.otherAccounts,
|
state.resultState is SearchBarResultState.Results -> {
|
||||||
isExpanded = state.isAccountListExpanded,
|
|
||||||
onToggleExpand = { state.eventSink(RoomSelectEvent.ToggleAccountListExpanded) },
|
|
||||||
onSelectAccount = { state.eventSink(RoomSelectEvent.SelectAccount(it)) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SelectedRoomsHelper(
|
|
||||||
selectedRooms = state.selectedRooms,
|
|
||||||
// showVerticalSpace only if there is no other accounts
|
|
||||||
showVerticalSpace = !state.showAccountSwitch,
|
|
||||||
)
|
|
||||||
if (state.hasRoomListError) {
|
|
||||||
RoomListError()
|
|
||||||
} else if (state.resultState is SearchBarResultState.Results) {
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = lazyListState,
|
state = lazyListState,
|
||||||
contentPadding = lazyColumnContentPadding,
|
contentPadding = lazyColumnContentPadding,
|
||||||
) {
|
) {
|
||||||
if (state.resultState.results.isNotEmpty()) {
|
if (state.resultState.results.isNotEmpty()) {
|
||||||
item {
|
item {
|
||||||
|
val headerText = if (searchQuery.isBlank()) {
|
||||||
|
stringResource(CommonStrings.common_header_rooms)
|
||||||
|
} else {
|
||||||
|
stringResource(CommonStrings.common_results_for, searchQuery)
|
||||||
|
}
|
||||||
ListSectionHeader(
|
ListSectionHeader(
|
||||||
title = stringResource(CommonStrings.common_header_rooms),
|
title = headerText,
|
||||||
hasDivider = false,
|
hasDivider = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -278,11 +273,45 @@ fun RoomSelectView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun NoResultsView(
|
||||||
|
searchQuery: String,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
// Scrollable, so that the account switch section can be displayed again by scrolling down
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(horizontal = 24.dp, vertical = 40.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(CommonStrings.common_no_results),
|
||||||
|
modifier = Modifier
|
||||||
|
.semantics {
|
||||||
|
heading()
|
||||||
|
},
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
style = ElementTheme.typography.fontHeadingMdBold,
|
||||||
|
color = ElementTheme.colors.textPrimary,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(CommonStrings.common_no_results_for, searchQuery),
|
||||||
|
modifier = Modifier.widthIn(max = 300.dp),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RoomListError() {
|
private fun RoomListError() {
|
||||||
IconTitleSubtitleMolecule(
|
IconTitleSubtitleMolecule(
|
||||||
|
|||||||
+27
-15
@@ -59,7 +59,6 @@ class RoomSelectPresenterTest {
|
|||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
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.maxNumberOfRooms).isEqualTo(10)
|
assertThat(initialState.maxNumberOfRooms).isEqualTo(10)
|
||||||
assertThat(initialState.canSelectMoreRooms).isTrue()
|
assertThat(initialState.canSelectMoreRooms).isTrue()
|
||||||
assertThat(initialState.selectedAccount).isEqualTo(MatrixUser(A_SESSION_ID))
|
assertThat(initialState.selectedAccount).isEqualTo(MatrixUser(A_SESSION_ID))
|
||||||
@@ -264,6 +263,33 @@ class RoomSelectPresenterTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - select another account keeps the search query and applies it to the rooms of this account`() = runTest {
|
||||||
|
val roomList2 = FakeDynamicRoomList(summaries = MutableStateFlow(listOf(aRoomSummary(roomId = A_ROOM_ID_2))))
|
||||||
|
val roomListServices = mapOf(
|
||||||
|
A_SESSION_ID to FakeRoomListService(createRoomListLambda = { FakeDynamicRoomList(summaries = MutableStateFlow(listOf(aRoomSummary()))) }),
|
||||||
|
A_SESSION_ID_2 to FakeRoomListService(createRoomListLambda = { roomList2 }),
|
||||||
|
)
|
||||||
|
val presenter = createRoomSelectPresenter(
|
||||||
|
mode = RoomSelectMode.Share,
|
||||||
|
sessionStore = aSessionStoreWithTwoAccounts(),
|
||||||
|
matrixClientProvider = FakeMatrixClientProvider { sessionId ->
|
||||||
|
Result.success(FakeMatrixClient(sessionId = sessionId, roomListService = roomListServices.getValue(sessionId)))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
val state = awaitLastSequentialItem()
|
||||||
|
state.searchQuery.setTextAndPlaceCursorAtEnd("query")
|
||||||
|
state.eventSink(RoomSelectEvent.SelectAccount(A_SESSION_ID_2))
|
||||||
|
val finalState = consumeItemsUntilPredicate {
|
||||||
|
it.selectedAccount.userId == A_SESSION_ID_2 && it.resultState is SearchBarResultState.Results
|
||||||
|
}.last()
|
||||||
|
assertThat(finalState.searchQuery.text.toString()).isEqualTo("query")
|
||||||
|
assertThat(finalState.showAccountSwitch).isTrue()
|
||||||
|
assertThat(roomList2.currentFilter.value).isEqualTo(RoomListFilter.NormalizedMatchRoomName("query"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - forward mode with several accounts does not show the account switch`() = runTest {
|
fun `present - forward mode with several accounts does not show the account switch`() = runTest {
|
||||||
val presenter = createRoomSelectPresenter(
|
val presenter = createRoomSelectPresenter(
|
||||||
@@ -284,18 +310,6 @@ class RoomSelectPresenterTest {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - toggle search active`() = runTest {
|
|
||||||
val presenter = createRoomSelectPresenter()
|
|
||||||
presenter.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
initialState.eventSink(RoomSelectEvent.ToggleSearchActive)
|
|
||||||
assertThat(awaitItem().isSearchActive).isTrue()
|
|
||||||
initialState.eventSink(RoomSelectEvent.ToggleSearchActive)
|
|
||||||
assertThat(awaitItem().isSearchActive).isFalse()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - update query`() = runTest {
|
fun `present - update query`() = runTest {
|
||||||
val roomSummary = aRoomSummary()
|
val roomSummary = aRoomSummary()
|
||||||
@@ -314,8 +328,6 @@ class RoomSelectPresenterTest {
|
|||||||
// Do not compare the lambda because they will be different. So copy the lambda from expectedRoomSummary to result
|
// Do not compare the lambda because they will be different. So copy the lambda from expectedRoomSummary to result
|
||||||
val result = (awaitItem().resultState as SearchBarResultState.Results).results
|
val result = (awaitItem().resultState as SearchBarResultState.Results).results
|
||||||
assertThat(result).isEqualTo(listOf(expectedRoomInfo))
|
assertThat(result).isEqualTo(listOf(expectedRoomInfo))
|
||||||
initialState.eventSink(RoomSelectEvent.ToggleSearchActive)
|
|
||||||
skipItems(1)
|
|
||||||
initialState.searchQuery.setTextAndPlaceCursorAtEnd("string not contained")
|
initialState.searchQuery.setTextAndPlaceCursorAtEnd("string not contained")
|
||||||
assertThat(
|
assertThat(
|
||||||
roomList.currentFilter.value
|
roomList.currentFilter.value
|
||||||
|
|||||||
+11
@@ -155,6 +155,17 @@ class RoomSelectViewTest : RobolectricTest() {
|
|||||||
onNodeWithText("Room 0").assertIsNotDisplayed()
|
onNodeWithText("Room 0").assertIsNotDisplayed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a message is displayed when no room matches the search query`() = runAndroidComposeUiTest {
|
||||||
|
setRoomSelectView(
|
||||||
|
aRoomSelectState(
|
||||||
|
searchQuery = "Test",
|
||||||
|
resultState = SearchBarResultState.NoResultsFound,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
onNodeWithText(activity!!.getString(CommonStrings.common_no_results)).assertIsDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `an error is displayed instead of the rooms if they cannot be loaded`() = runAndroidComposeUiTest<ComponentActivity> {
|
fun `an error is displayed instead of the rooms if they cannot be loaded`() = runAndroidComposeUiTest<ComponentActivity> {
|
||||||
setRoomSelectView(
|
setRoomSelectView(
|
||||||
|
|||||||
@@ -335,6 +335,7 @@ Reason: %1$s."</string>
|
|||||||
<string name="common_mute">"Mute"</string>
|
<string name="common_mute">"Mute"</string>
|
||||||
<string name="common_name">"Name"</string>
|
<string name="common_name">"Name"</string>
|
||||||
<string name="common_name_and_id">"%1$s (%2$s)"</string>
|
<string name="common_name_and_id">"%1$s (%2$s)"</string>
|
||||||
|
<string name="common_no_results_for">"There are no results for “%1$s”. Try a new search term."</string>
|
||||||
<string name="common_no_results">"No results"</string>
|
<string name="common_no_results">"No results"</string>
|
||||||
<string name="common_no_room_name">"No room name"</string>
|
<string name="common_no_room_name">"No room name"</string>
|
||||||
<string name="common_no_space_name">"No space name"</string>
|
<string name="common_no_space_name">"No space name"</string>
|
||||||
@@ -381,6 +382,7 @@ Reason: %1$s."</string>
|
|||||||
<string name="common_replying_to">"Replying to %1$s"</string>
|
<string name="common_replying_to">"Replying to %1$s"</string>
|
||||||
<string name="common_report_a_problem">"Report a problem"</string>
|
<string name="common_report_a_problem">"Report a problem"</string>
|
||||||
<string name="common_report_submitted">"Report submitted"</string>
|
<string name="common_report_submitted">"Report submitted"</string>
|
||||||
|
<string name="common_results_for">"Results for “%1$s”"</string>
|
||||||
<string name="common_rich_text_editor">"Rich text editor"</string>
|
<string name="common_rich_text_editor">"Rich text editor"</string>
|
||||||
<string name="common_role">"Role"</string>
|
<string name="common_role">"Role"</string>
|
||||||
<string name="common_room">"Room"</string>
|
<string name="common_room">"Room"</string>
|
||||||
|
|||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Reference in New Issue
Block a user