Room directory : fix pagination and add empty state.

This commit is contained in:
ganfra 2024-03-25 18:21:03 +01:00
parent 089fbe697a
commit 967c1b1a02
6 changed files with 91 additions and 60 deletions

View file

@ -22,5 +22,4 @@ sealed interface RoomDirectoryEvents {
data class Search(val query: String) : RoomDirectoryEvents data class Search(val query: String) : RoomDirectoryEvents
data object LoadMore : RoomDirectoryEvents data object LoadMore : RoomDirectoryEvents
data class JoinRoom(val roomId: RoomId) : RoomDirectoryEvents data class JoinRoom(val roomId: RoomId) : RoomDirectoryEvents
data class SearchActiveChange(val isActive: Boolean) : RoomDirectoryEvents
} }

View file

@ -26,14 +26,11 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import io.element.android.features.roomdirectory.impl.root.model.RoomDescriptionUiModel
import io.element.android.features.roomdirectory.impl.root.model.toUiModel import io.element.android.features.roomdirectory.impl.root.model.toUiModel
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -76,9 +73,6 @@ class RoomDirectoryPresenter @Inject constructor(
is RoomDirectoryEvents.Search -> { is RoomDirectoryEvents.Search -> {
searchQuery = event.query searchQuery = event.query
} }
is RoomDirectoryEvents.SearchActiveChange -> {
}
} }
} }
@ -90,23 +84,6 @@ class RoomDirectoryPresenter @Inject constructor(
) )
} }
@Composable
private fun searchResults(
filteredRooms: ImmutableList<RoomDescriptionUiModel>,
hasMoreToLoad: Boolean,
isSearchActive: Boolean,
): SearchBarResultState<ImmutableList<RoomDescriptionUiModel>> {
return if (!isSearchActive) {
SearchBarResultState.Initial()
} else {
if (filteredRooms.isEmpty() && !hasMoreToLoad) {
SearchBarResultState.NoResultsFound()
} else {
SearchBarResultState.Results(filteredRooms)
}
}
}
@Composable @Composable
private fun RoomDirectoryList.collectItemsAsState() = remember { private fun RoomDirectoryList.collectItemsAsState() = remember {
items.map { list -> items.map { list ->

View file

@ -17,6 +17,7 @@
package io.element.android.features.roomdirectory.impl.root package io.element.android.features.roomdirectory.impl.root
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -26,6 +27,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
@ -33,6 +35,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TextFieldColors import androidx.compose.material3.TextFieldColors
import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@ -49,6 +52,7 @@ import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.aliasScreenTitle import io.element.android.libraries.designsystem.theme.aliasScreenTitle
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.Icon 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.IconButton
import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Scaffold
@ -77,8 +81,8 @@ fun RoomDirectoryView(
state.eventSink(RoomDirectoryEvents.JoinRoom(roomId)) state.eventSink(RoomDirectoryEvents.JoinRoom(roomId))
}, },
modifier = Modifier modifier = Modifier
.padding(padding) .padding(padding)
.consumeWindowInsets(padding) .consumeWindowInsets(padding)
) )
} }
) )
@ -119,7 +123,10 @@ private fun RoomDirectoryContent(
) )
RoomDirectoryRoomList( RoomDirectoryRoomList(
roomDescriptions = state.roomDescriptions, roomDescriptions = state.roomDescriptions,
displayLoadMoreIndicator = state.displayLoadMoreIndicator,
displayEmptyState = state.displayEmptyState,
onResultClicked = onResultClicked, onResultClicked = onResultClicked,
onReachedLoadMore = { state.eventSink(RoomDirectoryEvents.LoadMore) },
) )
} }
} }
@ -127,18 +134,52 @@ private fun RoomDirectoryContent(
@Composable @Composable
private fun RoomDirectoryRoomList( private fun RoomDirectoryRoomList(
roomDescriptions: ImmutableList<RoomDescriptionUiModel>, roomDescriptions: ImmutableList<RoomDescriptionUiModel>,
displayLoadMoreIndicator: Boolean,
displayEmptyState: Boolean,
onResultClicked: (RoomId) -> Unit, onResultClicked: (RoomId) -> Unit,
onReachedLoadMore: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
LazyColumn( LazyColumn(modifier = modifier) {
modifier = modifier,
) {
items(roomDescriptions) { roomDescription -> items(roomDescriptions) { roomDescription ->
RoomDirectoryRoomRow( RoomDirectoryRoomRow(
roomDescription = roomDescription, roomDescription = roomDescription,
onClick = onResultClicked, onClick = onResultClicked,
) )
} }
if (displayEmptyState) {
item {
Text(
text = stringResource(id = CommonStrings.common_no_results),
style = ElementTheme.typography.fontBodyLgRegular,
color = ElementTheme.colors.textPlaceholder,
modifier = Modifier.padding(16.dp)
)
}
}
if (displayLoadMoreIndicator) {
item {
LoadMoreIndicator(modifier = Modifier.fillMaxWidth())
LaunchedEffect(Unit) {
onReachedLoadMore()
}
}
}
}
}
@Composable
private fun LoadMoreIndicator(modifier: Modifier = Modifier) {
Box(
modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(8.dp),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator(
strokeWidth = 2.dp,
)
} }
} }
@ -203,14 +244,14 @@ private fun RoomDirectoryRoomRow(
) { ) {
Row( Row(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { onClick(roomDescription.roomId) } .clickable { onClick(roomDescription.roomId) }
.padding( .padding(
top = 12.dp, top = 12.dp,
bottom = 12.dp, bottom = 12.dp,
start = 16.dp, start = 16.dp,
) )
.height(IntrinsicSize.Min), .height(IntrinsicSize.Min),
) { ) {
Avatar( Avatar(
avatarData = roomDescription.avatarData, avatarData = roomDescription.avatarData,
@ -218,8 +259,8 @@ private fun RoomDirectoryRoomRow(
) )
Column( Column(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.padding(start = 16.dp) .padding(start = 16.dp)
) { ) {
Text( Text(
text = roomDescription.name, text = roomDescription.name,
@ -241,8 +282,8 @@ private fun RoomDirectoryRoomRow(
text = stringResource(id = CommonStrings.action_join), text = stringResource(id = CommonStrings.action_join),
color = ElementTheme.colors.textSuccessPrimary, color = ElementTheme.colors.textSuccessPrimary,
modifier = Modifier modifier = Modifier
.align(Alignment.CenterVertically) .align(Alignment.CenterVertically)
.padding(start = 4.dp, end = 12.dp) .padding(start = 4.dp, end = 12.dp)
) )
} else { } else {
Spacer(modifier = Modifier.width(24.dp)) Spacer(modifier = Modifier.width(24.dp))

View file

@ -19,8 +19,8 @@ package io.element.android.libraries.matrix.api.roomdirectory
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface RoomDirectoryList { interface RoomDirectoryList {
suspend fun filter(filter: String?, batchSize: Int) suspend fun filter(filter: String?, batchSize: Int): Result<Unit>
suspend fun loadMore() suspend fun loadMore(): Result<Unit>
suspend fun hasMoreToLoad(): Boolean suspend fun hasMoreToLoad(): Boolean
val items: Flow<List<RoomDescription>> val items: Flow<List<RoomDescription>>
} }

View file

@ -42,27 +42,27 @@ class RoomDirectorySearchProcessor(
} }
} }
private suspend fun MutableList<RoomDescription>.applyUpdate(update: RoomDirectorySearchEntryUpdate) { private fun MutableList<RoomDescription>.applyUpdate(update: RoomDirectorySearchEntryUpdate) {
when (update) { when (update) {
is RoomDirectorySearchEntryUpdate.Append -> { is RoomDirectorySearchEntryUpdate.Append -> {
val roomSummaries = update.values.map(roomDescriptionMapper::map) val roomSummaries = update.values.map(roomDescriptionMapper::map)
addAll(roomSummaries) addAll(roomSummaries)
} }
is RoomDirectorySearchEntryUpdate.PushBack -> { is RoomDirectorySearchEntryUpdate.PushBack -> {
val roomSummary = roomDescriptionMapper.map(update.value) val roomDescription = roomDescriptionMapper.map(update.value)
add(roomSummary) add(roomDescription)
} }
is RoomDirectorySearchEntryUpdate.PushFront -> { is RoomDirectorySearchEntryUpdate.PushFront -> {
val roomSummary = roomDescriptionMapper.map(update.value) val roomDescription = roomDescriptionMapper.map(update.value)
add(0, roomSummary) add(0, roomDescription)
} }
is RoomDirectorySearchEntryUpdate.Set -> { is RoomDirectorySearchEntryUpdate.Set -> {
val roomSummary = roomDescriptionMapper.map(update.value) val roomDescription = roomDescriptionMapper.map(update.value)
this[update.index.toInt()] = roomSummary this[update.index.toInt()] = roomDescription
} }
is RoomDirectorySearchEntryUpdate.Insert -> { is RoomDirectorySearchEntryUpdate.Insert -> {
val roomSummary = roomDescriptionMapper.map(update.value) val roomDescription = roomDescriptionMapper.map(update.value)
add(update.index.toInt(), roomSummary) add(update.index.toInt(), roomDescription)
} }
is RoomDirectorySearchEntryUpdate.Remove -> { is RoomDirectorySearchEntryUpdate.Remove -> {
removeAt(update.index.toInt()) removeAt(update.index.toInt())

View file

@ -18,6 +18,7 @@ package io.element.android.libraries.matrix.impl.roomdirectory
import io.element.android.libraries.matrix.api.roomdirectory.RoomDescription import io.element.android.libraries.matrix.api.roomdirectory.RoomDescription
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
@ -32,12 +33,11 @@ import org.matrix.rustcomponents.sdk.RoomDirectorySearch as InnerRoomDirectorySe
class RustRoomDirectoryList( class RustRoomDirectoryList(
private val inner: InnerRoomDirectorySearch, private val inner: InnerRoomDirectorySearch,
private val sessionCoroutineScope: CoroutineScope, sessionCoroutineScope: CoroutineScope,
private val sessionDispatcher: CoroutineDispatcher, sessionDispatcher: CoroutineDispatcher,
) : RoomDirectoryList { ) : RoomDirectoryList {
private val _items = MutableSharedFlow<List<RoomDescription>>() private val _items = MutableSharedFlow<List<RoomDescription>>(replay = 1)
private val processor = RoomDirectorySearchProcessor(_items, sessionDispatcher, RoomDescriptionMapper()) private val processor = RoomDirectorySearchProcessor(_items, sessionDispatcher, RoomDescriptionMapper())
init { init {
@ -51,12 +51,26 @@ class RustRoomDirectoryList(
} }
} }
override suspend fun filter(filter: String?, batchSize: Int) { override suspend fun filter(filter: String?, batchSize: Int): Result<Unit> {
inner.search(filter, batchSize.toUInt()) return try {
inner.search(filter = filter, batchSize = batchSize.toUInt())
Result.success(Unit)
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
Result.failure(e)
}
} }
override suspend fun loadMore() { override suspend fun loadMore(): Result<Unit> {
inner.nextPage() return try {
inner.nextPage()
Result.success(Unit)
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
Result.failure(e)
}
} }
override suspend fun hasMoreToLoad(): Boolean { override suspend fun hasMoreToLoad(): Boolean {