RoomDirectory : continue improving interactions

This commit is contained in:
ganfra
2024-03-27 12:51:36 +01:00
parent bd01dacd85
commit 425eadd98f
14 changed files with 171 additions and 86 deletions

View File

@@ -25,4 +25,5 @@ android {
dependencies {
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.designsystem)
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright (c) 2024 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.roomdirectory.api
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.api.core.RoomId
data class RoomDescription(
val roomId: RoomId,
val name: String,
val description: String,
val avatarData: AvatarData,
val canBeJoined: Boolean,
)

View File

@@ -22,12 +22,12 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import io.element.android.features.roomdirectory.impl.root.model.toUiModel
import io.element.android.features.roomdirectory.impl.root.model.RoomDirectoryListState
import io.element.android.features.roomdirectory.impl.root.model.toFeatureModel
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runUpdatingState
@@ -36,9 +36,9 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
@@ -47,34 +47,43 @@ import javax.inject.Inject
class RoomDirectoryPresenter @Inject constructor(
private val dispatchers: CoroutineDispatchers,
private val matrixClient: MatrixClient,
roomDirectoryService: RoomDirectoryService,
private val roomDirectoryService: RoomDirectoryService,
) : Presenter<RoomDirectoryState> {
private val roomDirectoryList = roomDirectoryService.createRoomDirectoryList()
@Composable
override fun present(): RoomDirectoryState {
var loadingMore by remember {
mutableStateOf(false)
}
var searchQuery by rememberSaveable {
mutableStateOf("")
mutableStateOf<String?>(null)
}
val allRooms by roomDirectoryList.collectItemsAsState()
val hasMoreToLoad by produceState(initialValue = true, allRooms) {
value = roomDirectoryList.hasMoreToLoad()
val coroutineScope = rememberCoroutineScope()
val roomDirectoryList = remember {
roomDirectoryService.createRoomDirectoryList(coroutineScope)
}
val listState by roomDirectoryList.collectState()
val joinRoomAction: MutableState<AsyncAction<RoomId>> = remember {
mutableStateOf(AsyncAction.Uninitialized)
}
val coroutineScope = rememberCoroutineScope()
LaunchedEffect(searchQuery) {
if (searchQuery == null) return@LaunchedEffect
//debounce search query
delay(300)
//cancel load more right away
loadingMore = false
roomDirectoryList.filter(searchQuery, 20)
}
LaunchedEffect(loadingMore) {
if (loadingMore) {
roomDirectoryList.loadMore()
loadingMore = false
}
}
fun handleEvents(event: RoomDirectoryEvents) {
when (event) {
RoomDirectoryEvents.LoadMore -> {
coroutineScope.launch {
roomDirectoryList.loadMore()
}
loadingMore = true
}
is RoomDirectoryEvents.Search -> {
searchQuery = event.query
@@ -89,9 +98,9 @@ class RoomDirectoryPresenter @Inject constructor(
}
return RoomDirectoryState(
query = searchQuery,
roomDescriptions = allRooms,
displayLoadMoreIndicator = hasMoreToLoad,
query = searchQuery.orEmpty(),
roomDescriptions = listState.items,
displayLoadMoreIndicator = listState.hasMoreToLoad,
joinRoomAction = joinRoomAction.value,
eventSink = ::handleEvents
)
@@ -104,11 +113,12 @@ class RoomDirectoryPresenter @Inject constructor(
}
@Composable
private fun RoomDirectoryList.collectItemsAsState() = remember {
items.map { list ->
list
.map { roomDescription -> roomDescription.toUiModel() }
private fun RoomDirectoryList.collectState() = remember {
state.map {
val items = it.items
.map { roomDescription -> roomDescription.toFeatureModel() }
.toImmutableList()
RoomDirectoryListState(items = items, hasMoreToLoad = it.hasMoreToLoad)
}.flowOn(dispatchers.computation)
}.collectAsState(persistentListOf())
}.collectAsState(RoomDirectoryListState.Default)
}

View File

@@ -16,14 +16,14 @@
package io.element.android.features.roomdirectory.impl.root
import io.element.android.features.roomdirectory.impl.root.model.RoomDescriptionUiModel
import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.collections.immutable.ImmutableList
data class RoomDirectoryState(
val query: String,
val roomDescriptions: ImmutableList<RoomDescriptionUiModel>,
val roomDescriptions: ImmutableList<RoomDescription>,
val displayLoadMoreIndicator: Boolean,
val joinRoomAction: AsyncAction<RoomId>,
val eventSink: (RoomDirectoryEvents) -> Unit

View File

@@ -17,7 +17,7 @@
package io.element.android.features.roomdirectory.impl.root
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.roomdirectory.impl.root.model.RoomDescriptionUiModel
import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
@@ -32,7 +32,7 @@ open class RoomDirectorySearchStateProvider : PreviewParameterProvider<RoomDirec
aRoomDirectoryState(
query = "Element",
roomDescriptions = persistentListOf(
RoomDescriptionUiModel(
RoomDescription(
roomId = RoomId("@exa:matrix.org"),
name = "Element X Android",
description = "Element X is a secure, private and decentralized messenger.",
@@ -44,7 +44,7 @@ open class RoomDirectorySearchStateProvider : PreviewParameterProvider<RoomDirec
),
canBeJoined = true,
),
RoomDescriptionUiModel(
RoomDescription(
roomId = RoomId("@exi:matrix.org"),
name = "Element X iOS",
description = "Element X is a secure, private and decentralized messenger.",
@@ -64,7 +64,7 @@ open class RoomDirectorySearchStateProvider : PreviewParameterProvider<RoomDirec
fun aRoomDirectoryState(
query: String = "",
displayLoadMoreIndicator: Boolean = false,
roomDescriptions: ImmutableList<RoomDescriptionUiModel> = persistentListOf(),
roomDescriptions: ImmutableList<RoomDescription> = persistentListOf(),
joinRoomAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
) = RoomDirectoryState(
query = query,

View File

@@ -46,7 +46,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.roomdirectory.impl.root.model.RoomDescriptionUiModel
import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.button.BackButton
@@ -144,7 +144,7 @@ private fun RoomDirectoryContent(
@Composable
private fun RoomDirectoryRoomList(
roomDescriptions: ImmutableList<RoomDescriptionUiModel>,
roomDescriptions: ImmutableList<RoomDescription>,
displayLoadMoreIndicator: Boolean,
displayEmptyState: Boolean,
onResultClicked: (RoomId) -> Unit,
@@ -185,7 +185,7 @@ private fun LoadMoreIndicator(modifier: Modifier = Modifier) {
modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(8.dp),
.padding(24.dp),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator(
@@ -249,7 +249,7 @@ private fun SearchTextField(
@Composable
private fun RoomDirectoryRoomRow(
roomDescription: RoomDescriptionUiModel,
roomDescription: RoomDescription,
onClick: (RoomId) -> Unit,
modifier: Modifier = Modifier,
) {

View File

@@ -16,30 +16,22 @@
package io.element.android.features.roomdirectory.impl.root.model
import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.roomdirectory.RoomDescription
import io.element.android.libraries.matrix.api.roomdirectory.RoomDescription as MatrixRoomDescription
data class RoomDescriptionUiModel(
val roomId: RoomId,
val name: String,
val description: String,
val avatarData: AvatarData,
val canBeJoined: Boolean,
)
fun RoomDescription.toUiModel(): RoomDescriptionUiModel {
return RoomDescriptionUiModel(
fun MatrixRoomDescription.toFeatureModel(): RoomDescription {
return RoomDescription(
roomId = roomId,
name = name ?: "",
description = topic ?: "",
description = topic ?: alias ?: roomId.value,
avatarData = AvatarData(
id = roomId.value,
name = name ?: "",
url = avatarUrl,
size = AvatarSize.RoomDirectoryItem,
),
canBeJoined = joinRule == RoomDescription.JoinRule.PUBLIC,
canBeJoined = joinRule == MatrixRoomDescription.JoinRule.PUBLIC,
)
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright (c) 2024 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.roomdirectory.impl.root.model
import io.element.android.features.roomdirectory.api.RoomDescription
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
internal data class RoomDirectoryListState(
val hasMoreToLoad: Boolean,
val items: ImmutableList<RoomDescription>,
) {
companion object {
val Default = RoomDirectoryListState(
hasMoreToLoad = true,
items = persistentListOf()
)
}
}