[Room list] Search & menu improvements (#356)

* Remove settings menu item, start splitting search UI. Also, add `applyIf` and `circularReveal` modifiers.

* Split UI & logic for room list search

* Suppress `composed` warning, improve its debuggability

* Add content description to the user's avatar, fix window insets. Also, remove unused `SearchRoomListTopBar`.
This commit is contained in:
Jorge Martin Espinosa 2023-04-25 13:35:36 +02:00 committed by GitHub
parent b59ea181eb
commit 0234553bca
31 changed files with 534 additions and 199 deletions

View file

@ -71,8 +71,8 @@ class NetworkMonitorImpl @Inject constructor(
private fun listenToConnectionChanges() {
val request = NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
// .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
// .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build()
connectivityManager.registerNetworkCallback(request, callback)

View file

@ -20,4 +20,5 @@ sealed interface RoomListEvents {
data class UpdateFilter(val newFilter: String) : RoomListEvents
data class UpdateVisibleRange(val range: IntRange) : RoomListEvents
object DismissRequestVerificationPrompt : RoomListEvents
object ToggleSearchResults : RoomListEvents
}

View file

@ -79,6 +79,7 @@ class RoomListPresenter @Inject constructor(
Timber.v("RoomSummaries size = ${roomSummaries.size}")
val mappedRoomSummaries: MutableState<ImmutableList<RoomListRoomSummary>> = remember { mutableStateOf(persistentListOf()) }
val filteredRoomSummaries: MutableState<ImmutableList<RoomListRoomSummary>> = remember {
mutableStateOf(persistentListOf())
}
@ -101,41 +102,51 @@ class RoomListPresenter @Inject constructor(
derivedStateOf { sessionVerifiedStatus == SessionVerifiedStatus.NotVerified && !verificationPromptDismissed }
}
var displaySearchResults by rememberSaveable { mutableStateOf(false) }
fun handleEvents(event: RoomListEvents) {
when (event) {
is RoomListEvents.UpdateFilter -> filter = event.newFilter
is RoomListEvents.UpdateVisibleRange -> updateVisibleRange(event.range)
RoomListEvents.DismissRequestVerificationPrompt -> verificationPromptDismissed = true
RoomListEvents.ToggleSearchResults -> {
if (displaySearchResults) {
filter = ""
}
displaySearchResults =! displaySearchResults
}
}
}
LaunchedEffect(roomSummaries, filter) {
filteredRoomSummaries.value = updateFilteredRoomSummaries(roomSummaries, filter)
mappedRoomSummaries.value = if (roomSummaries.isEmpty()) {
RoomListRoomSummaryPlaceholders.createFakeList(16).toImmutableList()
} else {
mapRoomSummaries(roomSummaries).toImmutableList()
}
filteredRoomSummaries.value = updateFilteredRoomSummaries(mappedRoomSummaries.value, filter)
}
val snackbarMessage = handleSnackbarMessage(snackbarDispatcher)
return RoomListState(
matrixUser = matrixUser.value,
roomList = filteredRoomSummaries.value,
roomList = mappedRoomSummaries.value,
filter = filter,
filteredRoomList = filteredRoomSummaries.value,
displayVerificationPrompt = displayVerificationPrompt,
snackbarMessage = snackbarMessage,
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
displayInvites = invites.isNotEmpty(),
displaySearchResults = displaySearchResults,
eventSink = ::handleEvents
)
}
private suspend fun updateFilteredRoomSummaries(roomSummaries: List<RoomSummary>?, filter: String): ImmutableList<RoomListRoomSummary> {
if (roomSummaries.isNullOrEmpty()) {
return RoomListRoomSummaryPlaceholders.createFakeList(16).toImmutableList()
}
val mappedRoomSummaries = mapRoomSummaries(roomSummaries)
return if (filter.isEmpty()) {
mappedRoomSummaries
} else {
mappedRoomSummaries.filter { it.name.contains(filter, ignoreCase = true) }
private fun updateFilteredRoomSummaries(mappedRoomSummaries: ImmutableList<RoomListRoomSummary>, filter: String): ImmutableList<RoomListRoomSummary> {
return when {
filter.isEmpty() -> emptyList()
else -> mappedRoomSummaries.filter { it.name.contains(filter, ignoreCase = true) }
}.toImmutableList()
}

View file

@ -26,10 +26,12 @@ import kotlinx.collections.immutable.ImmutableList
data class RoomListState(
val matrixUser: MatrixUser?,
val roomList: ImmutableList<RoomListRoomSummary>,
val filter: String,
val filter: String?,
val filteredRoomList: ImmutableList<RoomListRoomSummary>,
val displayVerificationPrompt: Boolean,
val hasNetworkConnection: Boolean,
val snackbarMessage: SnackbarMessage?,
val displayInvites: Boolean,
val displaySearchResults: Boolean,
val eventSink: (RoomListEvents) -> Unit
)

View file

@ -36,6 +36,8 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
aRoomListState().copy(snackbarMessage = SnackbarMessage(StringR.string.common_verification_complete)),
aRoomListState().copy(hasNetworkConnection = false),
aRoomListState().copy(displayInvites = true),
aRoomListState().copy(displaySearchResults = true, filter = "", filteredRoomList = persistentListOf()),
aRoomListState().copy(displaySearchResults = true),
)
}
@ -43,10 +45,12 @@ internal fun aRoomListState() = RoomListState(
matrixUser = MatrixUser(id = UserId("@id:domain"), username = "User#1", avatarData = AvatarData("@id:domain", "U")),
roomList = aRoomListRoomSummaryList(),
filter = "filter",
filteredRoomList = aRoomListRoomSummaryList(),
hasNetworkConnection = true,
snackbarMessage = null,
displayVerificationPrompt = false,
displayInvites = false,
displaySearchResults = false,
eventSink = {}
)

View file

@ -64,6 +64,8 @@ import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorVi
import io.element.android.features.roomlist.impl.components.RoomListTopBar
import io.element.android.features.roomlist.impl.components.RoomSummaryRow
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.features.roomlist.impl.search.RoomListSearchResultContent
import io.element.android.features.roomlist.impl.search.RoomListSearchResultView
import io.element.android.libraries.designsystem.ElementTextStyles
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
@ -91,15 +93,27 @@ fun RoomListView(
onCreateRoomClicked: () -> Unit = {},
onInvitesClicked: () -> Unit = {},
) {
RoomListContent(
state = state,
modifier = modifier,
onRoomClicked = onRoomClicked,
onOpenSettings = onOpenSettings,
onVerifyClicked = onVerifyClicked,
onCreateRoomClicked = onCreateRoomClicked,
onInvitesClicked = onInvitesClicked,
)
Column(modifier = modifier) {
ConnectivityIndicatorView(isOnline = state.hasNetworkConnection)
Box {
RoomListContent(
state = state,
onRoomClicked = onRoomClicked,
onOpenSettings = onOpenSettings,
onVerifyClicked = onVerifyClicked,
onCreateRoomClicked = onCreateRoomClicked,
onInvitesClicked = onInvitesClicked,
)
// This overlaid view will only be visible when state.displaySearchResults is true
RoomListSearchResultView(
state = state,
onRoomClicked = onRoomClicked,
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@ -163,16 +177,14 @@ fun RoomListContent(
Scaffold(
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
Column {
ConnectivityIndicatorView(isOnline = state.hasNetworkConnection)
RoomListTopBar(
matrixUser = state.matrixUser,
filter = state.filter,
onFilterChanged = { state.eventSink(RoomListEvents.UpdateFilter(it)) },
onOpenSettings = onOpenSettings,
scrollBehavior = scrollBehavior,
)
}
RoomListTopBar(
matrixUser = state.matrixUser,
areSearchResultsDisplayed = state.displaySearchResults,
onFilterChanged = { state.eventSink(RoomListEvents.UpdateFilter(it)) },
onToggleSearch = { state.eventSink(RoomListEvents.ToggleSearchResults) },
onOpenSettings = onOpenSettings,
scrollBehavior = scrollBehavior,
)
},
content = { padding ->
Column(
@ -306,7 +318,7 @@ internal fun PreviewRequestVerificationHeaderDark() {
}
}
private fun RoomListRoomSummary.contentType() = isPlaceholder
internal fun RoomListRoomSummary.contentType() = isPlaceholder
@Preview
@Composable
@ -322,3 +334,11 @@ internal fun RoomListViewDarkPreview(@PreviewParameter(RoomListStateProvider::cl
private fun ContentToPreview(state: RoomListState) {
RoomListView(state)
}
@Preview
@Composable
internal fun RoomListSearchResultContentPreview() {
ElementPreviewLight {
RoomListSearchResultContent(state = aRoomListState(), onRoomClicked = {})
}
}

View file

@ -20,57 +20,40 @@ package io.element.android.features.roomlist.impl.components
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.ContentAlpha
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.element.android.features.roomlist.impl.R
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.form.textFieldState
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.MediumTopAppBar
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.utils.LogCompositions
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.ui.model.MatrixUser
import io.element.android.libraries.ui.strings.R as StringR
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RoomListTopBar(
matrixUser: MatrixUser?,
filter: String,
areSearchResultsDisplayed: Boolean,
onFilterChanged: (String) -> Unit,
onToggleSearch: () -> Unit,
onOpenSettings: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior,
modifier: Modifier = Modifier,
@ -79,124 +62,26 @@ fun RoomListTopBar(
tag = "RoomListScreen",
msg = "TopBar"
)
var searchWidgetStateIsOpened by rememberSaveable { mutableStateOf(false) }
fun closeFilter() {
onFilterChanged("")
searchWidgetStateIsOpened = false
}
BackHandler(enabled = searchWidgetStateIsOpened) {
BackHandler(enabled = areSearchResultsDisplayed) {
closeFilter()
onToggleSearch()
}
if (searchWidgetStateIsOpened) {
SearchRoomListTopBar(
text = filter,
onFilterChanged = onFilterChanged,
onCloseClicked = ::closeFilter,
scrollBehavior = scrollBehavior,
modifier = modifier,
)
} else {
DefaultRoomListTopBar(
matrixUser = matrixUser,
onOpenSettings = onOpenSettings,
onSearchClicked = {
searchWidgetStateIsOpened = true
},
scrollBehavior = scrollBehavior,
modifier = modifier,
)
}
}
@Composable
fun SearchRoomListTopBar(
text: String,
scrollBehavior: TopAppBarScrollBehavior,
modifier: Modifier = Modifier,
onFilterChanged: (String) -> Unit = {},
onCloseClicked: () -> Unit = {},
) {
var filterState by textFieldState(stateValue = text)
val focusRequester = remember { FocusRequester() }
TopAppBar(
modifier = modifier
.nestedScroll(scrollBehavior.nestedScrollConnection),
title = {
TextField(
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
value = filterState,
textStyle = TextStyle(
fontSize = 17.sp
),
onValueChange = {
filterState = it
onFilterChanged(it)
},
placeholder = {
Text(
text = stringResource(id = StringR.string.action_search),
color = MaterialTheme.colorScheme.onBackground.copy(alpha = ContentAlpha.medium)
)
},
singleLine = true,
trailingIcon = {
if (text.isNotEmpty()) {
IconButton(
onClick = {
onFilterChanged("")
}
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = "clear",
tint = MaterialTheme.colorScheme.onBackground
)
}
}
},
)
},
navigationIcon = {
IconButton(
onClick = {
onCloseClicked()
}
) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = "close",
tint = MaterialTheme.colorScheme.onBackground
)
}
},
windowInsets = WindowInsets(0.dp)
)
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}
@Preview
@Composable
internal fun SearchRoomListTopBarLightPreview() = ElementPreviewLight { SearchRoomListTopBarPreview() }
@Preview
@Composable
internal fun SearchRoomListTopBarDarkPreview() = ElementPreviewDark { SearchRoomListTopBarPreview() }
@Composable
private fun SearchRoomListTopBarPreview() {
SearchRoomListTopBar(
text = "Hello",
scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()),
DefaultRoomListTopBar(
matrixUser = matrixUser,
onOpenSettings = onOpenSettings,
onSearchClicked = onToggleSearch,
scrollBehavior = scrollBehavior,
modifier = modifier,
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun DefaultRoomListTopBar(
matrixUser: MatrixUser?,
@ -216,8 +101,8 @@ private fun DefaultRoomListTopBar(
},
navigationIcon = {
if (matrixUser != null) {
IconButton(onClick = {}) {
Avatar(matrixUser.avatarData)
IconButton(onClick = onOpenSettings) {
Avatar(matrixUser.avatarData, contentDescription = stringResource(StringR.string.common_settings))
}
}
},
@ -225,12 +110,7 @@ private fun DefaultRoomListTopBar(
IconButton(
onClick = onSearchClicked
) {
Icon(Icons.Default.Search, contentDescription = "search")
}
IconButton(
onClick = onOpenSettings
) {
Icon(Icons.Default.Settings, contentDescription = "Settings")
Icon(Icons.Default.Search, contentDescription = stringResource(StringR.string.action_search))
}
},
scrollBehavior = scrollBehavior,
@ -246,6 +126,7 @@ internal fun DefaultRoomListTopBarLightPreview() = ElementPreviewLight { Default
@Composable
internal fun DefaultRoomListTopBarDarkPreview() = ElementPreviewDark { DefaultRoomListTopBarPreview() }
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun DefaultRoomListTopBarPreview() {
DefaultRoomListTopBar(

View file

@ -0,0 +1,201 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.roomlist.impl.search
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import io.element.android.features.roomlist.impl.RoomListEvents
import io.element.android.features.roomlist.impl.RoomListState
import io.element.android.features.roomlist.impl.components.RoomSummaryRow
import io.element.android.features.roomlist.impl.contentType
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.modifiers.applyIf
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.utils.copy
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.ui.strings.R
@Composable
internal fun RoomListSearchResultView(
state: RoomListState,
onRoomClicked: (RoomId) -> Unit,
modifier: Modifier = Modifier,
) {
AnimatedVisibility(
visible = state.displaySearchResults,
enter = fadeIn(),
exit = fadeOut(),
) {
Column(
modifier = modifier
.applyIf(state.displaySearchResults, ifTrue = {
// Disable input interaction to underlying views
pointerInput(Unit) {}
})
) {
if (state.displaySearchResults) {
RoomListSearchResultContent(state = state, onRoomClicked = onRoomClicked)
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun RoomListSearchResultContent(
state: RoomListState,
onRoomClicked: (RoomId) -> Unit,
modifier: Modifier = Modifier,
) {
val borderColor = MaterialTheme.colorScheme.tertiary
val strokeWidth = 1.dp
fun onBackButtonPressed() {
state.eventSink(RoomListEvents.ToggleSearchResults)
}
fun onRoomClicked(room: RoomListRoomSummary) {
if (room.roomId == null) return
onRoomClicked(room.roomId)
}
Scaffold(
modifier = modifier,
topBar = {
TopAppBar(
modifier = Modifier.drawBehind {
drawLine(
color = borderColor,
start = Offset(0f, size.height),
end = Offset(size.width, size.height),
strokeWidth = strokeWidth.value
)
},
navigationIcon = { BackButton(onClick = ::onBackButtonPressed) },
title = {
val filter = state.filter.orEmpty()
val focusRequester = FocusRequester()
TextField(
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
value = filter,
onValueChange = { state.eventSink(RoomListEvents.UpdateFilter(it)) },
colors = TextFieldDefaults.textFieldColors(
containerColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
errorIndicatorColor = Color.Transparent,
disabledIndicatorColor = Color.Transparent
),
trailingIcon = {
if (filter.isNotEmpty()) {
IconButton(onClick = {
state.eventSink(RoomListEvents.UpdateFilter(""))
}) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(R.string.action_cancel)
)
}
}
}
)
LaunchedEffect(state.displaySearchResults) {
if (state.displaySearchResults) {
focusRequester.requestFocus()
}
}
},
windowInsets = TopAppBarDefaults.windowInsets.copy(top = 0)
)
}
) { padding ->
val lazyListState = rememberLazyListState()
val visibleRange by remember {
derivedStateOf {
val layoutInfo = lazyListState.layoutInfo
val firstItemIndex = layoutInfo.visibleItemsInfo.firstOrNull()?.index ?: 0
val size = layoutInfo.visibleItemsInfo.size
firstItemIndex until firstItemIndex + size
}
}
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override suspend fun onPostFling(
consumed: Velocity,
available: Velocity
): Velocity {
state.eventSink(RoomListEvents.UpdateVisibleRange(visibleRange))
return super.onPostFling(consumed, available)
}
}
}
Column(
modifier = Modifier
.padding(padding)
) {
LazyColumn(
modifier = Modifier
.weight(1f)
.nestedScroll(nestedScrollConnection),
state = lazyListState,
) {
items(
items = state.filteredRoomList,
contentType = { room -> room.contentType() },
) { room ->
RoomSummaryRow(room = room, onClick = ::onRoomClicked)
}
}
}
}
}

View file

@ -112,6 +112,8 @@ class RoomListPresenterTests {
withUserState.eventSink.invoke(RoomListEvents.UpdateFilter("t"))
val withFilterState = awaitItem()
Truth.assertThat(withFilterState.filter).isEqualTo("t")
cancelAndIgnoreRemainingEvents()
}
}
@ -168,17 +170,18 @@ class RoomListPresenterTests {
val loadedState = awaitItem()
// Test filtering with result
loadedState.eventSink.invoke(RoomListEvents.UpdateFilter(A_ROOM_NAME.substring(0, 3)))
skipItems(1) // Filter update
val withNotFilteredRoomState = awaitItem()
Truth.assertThat(withNotFilteredRoomState.filter).isEqualTo(A_ROOM_NAME.substring(0, 3))
Truth.assertThat(withNotFilteredRoomState.roomList.size).isEqualTo(1)
Truth.assertThat(withNotFilteredRoomState.roomList.first())
Truth.assertThat(withNotFilteredRoomState.filteredRoomList.size).isEqualTo(1)
Truth.assertThat(withNotFilteredRoomState.filteredRoomList.first())
.isEqualTo(aRoomListRoomSummary)
// Test filtering without result
withNotFilteredRoomState.eventSink.invoke(RoomListEvents.UpdateFilter("tada"))
skipItems(1) // Filter update
val withFilteredRoomState = awaitItem()
Truth.assertThat(withFilteredRoomState.filter).isEqualTo("tada")
Truth.assertThat(withFilteredRoomState.roomList).isEmpty()
Truth.assertThat(withFilteredRoomState.filteredRoomList).isEmpty()
}
}