[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:
parent
b59ea181eb
commit
0234553bca
31 changed files with 534 additions and 199 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = {})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue