diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListScreen.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListScreen.kt index 554030f26b..a6aabfdf3f 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListScreen.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListScreen.kt @@ -2,9 +2,16 @@ package io.element.android.x.features.roomlist +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.material3.TopAppBarDefaults @@ -14,6 +21,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success import com.airbnb.mvrx.compose.collectAsState @@ -22,6 +30,7 @@ import io.element.android.x.core.compose.LogCompositions import io.element.android.x.designsystem.ElementXTheme import io.element.android.x.designsystem.components.ProgressDialog import io.element.android.x.designsystem.components.avatar.AvatarData +import io.element.android.x.features.roomlist.components.RoomFilter import io.element.android.x.features.roomlist.components.RoomItem import io.element.android.x.features.roomlist.components.RoomListTopBar import io.element.android.x.features.roomlist.model.MatrixUser @@ -37,6 +46,7 @@ fun RoomListScreen( ) { val viewModel: RoomListViewModel = mavericksViewModel() val logoutAction by viewModel.collectAsState(RoomListViewState::logoutAction) + val filter by viewModel.collectAsState(RoomListViewState::filter) if (logoutAction is Success) { onSuccessLogout() return @@ -49,7 +59,9 @@ fun RoomListScreen( matrixUser = matrixUser(), onRoomClicked = onRoomClicked, onLogoutClicked = viewModel::logout, - isLoginOut = logoutAction is Loading + isLoginOut = logoutAction is Loading, + filter = filter, + onFilterChanged = viewModel::filterRoom, ) } @@ -58,10 +70,13 @@ fun RoomListContent( roomSummaries: List, matrixUser: MatrixUser?, onRoomClicked: (RoomId) -> Unit, + filter: String, + onFilterChanged: (String) -> Unit, onLogoutClicked: () -> Unit, isLoginOut: Boolean, ) { val appBarState = rememberTopAppBarState() + val lazyListState = rememberLazyListState() val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(appBarState) LogCompositions(tag = "RoomListScreen", msg = "Content") Scaffold( @@ -70,10 +85,24 @@ fun RoomListContent( RoomListTopBar(matrixUser, onLogoutClicked, scrollBehavior) }, content = { padding -> - LazyColumn(modifier = Modifier.padding(padding)) { - items(roomSummaries) { room -> - RoomItem(room = room) { - onRoomClicked(it) + Column(modifier = Modifier.padding(padding)) { + RoomFilter( + modifier = Modifier + .fillMaxWidth() + .animateContentSize(animationSpec = tween(durationMillis = 300)) + .height(if (lazyListState.isScrolled()) 0.dp else 56.dp) + .padding(horizontal = 16.dp, vertical = 4.dp), + filter = filter, + onFilterChanged = onFilterChanged + ) + LazyColumn( + modifier = Modifier.weight(1f), + state = lazyListState, + ) { + items(roomSummaries) { room -> + RoomItem(room = room) { + onRoomClicked(it) + } } } } @@ -84,6 +113,9 @@ fun RoomListContent( } } +private fun LazyListState.isScrolled(): Boolean { + return firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0 +} @Preview @Composable @@ -94,7 +126,9 @@ private fun PreviewableRoomListContent() { matrixUser = MatrixUser("User#1", avatarData = AvatarData("U")), onRoomClicked = {}, onLogoutClicked = {}, - isLoginOut = false + filter = "filter", + onFilterChanged = {}, + isLoginOut = false, ) } } @@ -108,7 +142,9 @@ private fun PreviewableDarkRoomListContent() { matrixUser = MatrixUser("User#1", avatarData = AvatarData("U")), onRoomClicked = {}, onLogoutClicked = {}, - isLoginOut = true + filter = "filter", + onFilterChanged = {}, + isLoginOut = true, ) } } diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListViewModel.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListViewModel.kt index 0b222413a0..6aef08b98a 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListViewModel.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListViewModel.kt @@ -14,6 +14,8 @@ import io.element.android.x.matrix.media.MediaResolver import io.element.android.x.matrix.room.RoomSummary import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -56,6 +58,14 @@ class RoomListViewModel( } } + fun filterRoom(filter: String) { + setState { + copy( + filter = filter + ) + } + } + private fun handleInit() { suspend { val userAvatarUrl = client.loadUserAvatarURLString().getOrNull() @@ -75,15 +85,27 @@ class RoomListViewModel( copy(user = it) } - client.roomSummaryDataSource().roomSummaries() - .map(::mapRoomSummaries) - .flowOn(Dispatchers.Default) + // Observe the room list and the filter + combine( + client.roomSummaryDataSource().roomSummaries() + .map(::mapRoomSummaries) + .flowOn(Dispatchers.Default), + stateFlow + .map { it.filter } + .distinctUntilChanged(), + ) { list, filter -> + if (filter.isEmpty()) { + list + } else { + list.filter { it.name.contains(filter, ignoreCase = true) } + } + } .execute { copy( rooms = when { it is Loading || // Note: this second case will prevent to handle correctly the empty case - (it is Success && it().isEmpty()) -> { + (it is Success && it().isEmpty() && filter.isEmpty()) -> { // Show fake placeholders to avoid having empty screen Loading(createFakePlaceHolders()) } diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/components/RoomFilter.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/components/RoomFilter.kt new file mode 100644 index 0000000000..b2cbfb6ce6 --- /dev/null +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/components/RoomFilter.kt @@ -0,0 +1,54 @@ +package io.element.android.x.features.roomlist.components + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RoomFilter( + modifier: Modifier = Modifier, + filter: String, + onFilterChanged: (String) -> Unit +) { + TextField( + modifier = modifier, + value = filter, + onValueChange = onFilterChanged, + //label = { + // Text(text = "Search") + //}, + leadingIcon = { + Icon( + imageVector = Icons.Filled.Search, + contentDescription = null + ) + }, + trailingIcon = if (filter.isNotEmpty()) { + { + IconButton(onClick = { onFilterChanged("") }) { + Icon( + imageVector = Icons.Filled.Clear, + contentDescription = null + ) + } + } + } else null + ) +} + +@Composable +@Preview +private fun RoomFilterPreview() { + RoomFilter( + filter = "", + onFilterChanged = {} + ) +} \ No newline at end of file diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListViewState.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListViewState.kt index 4bc97316bd..86414c4a0e 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListViewState.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListViewState.kt @@ -7,7 +7,9 @@ import io.element.android.x.matrix.core.RoomId data class RoomListViewState( val user: Async = Uninitialized, + // Will contain the filtered rooms, using ::filter (if filter is not empty) val rooms: Async> = Uninitialized, + val filter: String = "", val canLoadMore: Boolean = false, val logoutAction: Async = Uninitialized, val roomsById: Map = emptyMap()