RoomDirectory : continue implementing the search

This commit is contained in:
ganfra 2024-03-25 11:17:19 +01:00
parent 21dfb46b1e
commit 089fbe697a
13 changed files with 190 additions and 247 deletions

View file

@ -21,73 +21,98 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
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.datasource.RoomDirectoryDataSource import io.element.android.features.roomdirectory.impl.root.model.RoomDescriptionUiModel
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.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.core.RoomId import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList
import kotlinx.coroutines.CoroutineScope import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
class RoomDirectoryPresenter @Inject constructor( class RoomDirectoryPresenter @Inject constructor(
private val client: MatrixClient, private val client: MatrixClient,
private val dataSource: RoomDirectoryDataSource, private val roomDirectoryService: RoomDirectoryService,
) : Presenter<RoomDirectoryState> { ) : Presenter<RoomDirectoryState> {
private val roomDirectoryList = roomDirectoryService.createRoomDirectoryList()
@Composable @Composable
override fun present(): RoomDirectoryState { override fun present(): RoomDirectoryState {
var searchQuery by rememberSaveable { var searchQuery by rememberSaveable {
mutableStateOf("") mutableStateOf("")
} }
var isSearchActive by rememberSaveable { val allRooms by roomDirectoryList.collectItemsAsState()
mutableStateOf(false) val hasMoreToLoad by produceState(initialValue = true, allRooms) {
value = roomDirectoryList.hasMoreToLoad()
} }
val roomSummaries by dataSource.all.collectAsState()
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
LaunchedEffect(searchQuery) { LaunchedEffect(searchQuery) {
dataSource.updateSearchQuery(searchQuery) roomDirectoryList.filter(searchQuery, 20)
} }
fun handleEvents(event: RoomDirectoryEvents) { fun handleEvents(event: RoomDirectoryEvents) {
when (event) { when (event) {
is RoomDirectoryEvents.JoinRoom -> { is RoomDirectoryEvents.JoinRoom -> {
coroutineScope.joinRoom(event.roomId) //coroutineScope.joinRoom(event.roomId)
} }
RoomDirectoryEvents.LoadMore -> { RoomDirectoryEvents.LoadMore -> {
coroutineScope.launch { coroutineScope.launch {
dataSource.loadMore() roomDirectoryList.loadMore()
} }
} }
is RoomDirectoryEvents.Search -> { is RoomDirectoryEvents.Search -> {
searchQuery = event.query searchQuery = event.query
} }
is RoomDirectoryEvents.SearchActiveChange -> { is RoomDirectoryEvents.SearchActiveChange -> {
isSearchActive = event.isActive
if (!isSearchActive) {
searchQuery = ""
}
} }
} }
} }
return RoomDirectoryState( return RoomDirectoryState(
query = searchQuery, query = searchQuery,
isSearchActive = isSearchActive, roomDescriptions = allRooms,
roomSummaries = roomSummaries, displayLoadMoreIndicator = hasMoreToLoad,
searchResults = SearchBarResultState.Initial(),
eventSink = ::handleEvents eventSink = ::handleEvents
) )
} }
private fun CoroutineScope.joinRoom(roomId: RoomId) = launch { @Composable
client.getRoom(roomId)?.join() 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
private fun RoomDirectoryList.collectItemsAsState() = remember {
items.map { list ->
list
.map { roomDescription -> roomDescription.toUiModel() }
.toImmutableList()
}.flowOn(Dispatchers.Default)
}.collectAsState(persistentListOf())
} }

View file

@ -16,14 +16,14 @@
package io.element.android.features.roomdirectory.impl.root package io.element.android.features.roomdirectory.impl.root
import io.element.android.features.roomdirectory.impl.root.model.RoomDirectoryRoomSummary import io.element.android.features.roomdirectory.impl.root.model.RoomDescriptionUiModel
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
data class RoomDirectoryState( data class RoomDirectoryState(
val query: String, val query: String,
val roomSummaries: ImmutableList<RoomDirectoryRoomSummary>, val roomDescriptions: ImmutableList<RoomDescriptionUiModel>,
val searchResults: SearchBarResultState<ImmutableList<RoomDirectoryRoomSummary>>, val displayLoadMoreIndicator: Boolean,
val isSearchActive: Boolean,
val eventSink: (RoomDirectoryEvents) -> Unit val eventSink: (RoomDirectoryEvents) -> Unit
) ) {
val displayEmptyState = roomDescriptions.isEmpty() && !displayLoadMoreIndicator
}

View file

@ -17,7 +17,7 @@
package io.element.android.features.roomdirectory.impl.root package io.element.android.features.roomdirectory.impl.root
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.roomdirectory.impl.root.model.RoomDirectoryRoomSummary import io.element.android.features.roomdirectory.impl.root.model.RoomDescriptionUiModel
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
@ -31,8 +31,8 @@ open class RoomDirectorySearchStateProvider : PreviewParameterProvider<RoomDirec
aRoomDirectoryState(), aRoomDirectoryState(),
aRoomDirectoryState( aRoomDirectoryState(
query = "Element", query = "Element",
roomSummaries = persistentListOf( roomDescriptions = persistentListOf(
RoomDirectoryRoomSummary( RoomDescriptionUiModel(
roomId = RoomId("@exa:matrix.org"), roomId = RoomId("@exa:matrix.org"),
name = "Element X Android", name = "Element X Android",
description = "Element X is a secure, private and decentralized messenger.", description = "Element X is a secure, private and decentralized messenger.",
@ -40,11 +40,11 @@ open class RoomDirectorySearchStateProvider : PreviewParameterProvider<RoomDirec
id = "@exa:matrix.org", id = "@exa:matrix.org",
name = "Element X Android", name = "Element X Android",
url = null, url = null,
size = AvatarSize.RoomDirectorySearchItem size = AvatarSize.RoomDirectoryItem
), ),
canBeJoined = true, canBeJoined = true,
), ),
RoomDirectoryRoomSummary( RoomDescriptionUiModel(
roomId = RoomId("@exi:matrix.org"), roomId = RoomId("@exi:matrix.org"),
name = "Element X iOS", name = "Element X iOS",
description = "Element X is a secure, private and decentralized messenger.", description = "Element X is a secure, private and decentralized messenger.",
@ -52,7 +52,7 @@ open class RoomDirectorySearchStateProvider : PreviewParameterProvider<RoomDirec
id = "@exi:matrix.org", id = "@exi:matrix.org",
name = "Element X iOS", name = "Element X iOS",
url = null, url = null,
size = AvatarSize.RoomDirectorySearchItem size = AvatarSize.RoomDirectoryItem
), ),
canBeJoined = false, canBeJoined = false,
) )
@ -64,12 +64,12 @@ open class RoomDirectorySearchStateProvider : PreviewParameterProvider<RoomDirec
fun aRoomDirectoryState( fun aRoomDirectoryState(
query: String = "", query: String = "",
isSearchActive: Boolean = false, isSearchActive: Boolean = false,
roomSummaries: ImmutableList<RoomDirectoryRoomSummary> = persistentListOf(), displayLoadMoreIndicator: Boolean = false,
searchResults: SearchBarResultState<ImmutableList<RoomDirectoryRoomSummary>> = SearchBarResultState.Initial(), roomDescriptions: ImmutableList<RoomDescriptionUiModel> = persistentListOf(),
searchResults: SearchBarResultState<ImmutableList<RoomDescriptionUiModel>> = SearchBarResultState.Initial(),
) = RoomDirectoryState( ) = RoomDirectoryState(
query = query, query = query,
isSearchActive = isSearchActive, roomDescriptions = roomDescriptions,
roomSummaries = roomSummaries, displayLoadMoreIndicator = displayLoadMoreIndicator,
searchResults = searchResults,
eventSink = {}, eventSink = {},
) )

View file

@ -17,37 +17,33 @@
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.Arrangement
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
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth 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.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.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
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.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.graphics.Color
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.roomdirectory.impl.root.model.RoomDirectoryRoomSummary import io.element.android.features.roomdirectory.impl.root.model.RoomDescriptionUiModel
import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.button.BackButton 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
@ -56,7 +52,6 @@ import io.element.android.libraries.designsystem.theme.aliasScreenTitle
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
import io.element.android.libraries.designsystem.theme.components.SearchBar
import io.element.android.libraries.designsystem.theme.components.Text 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.TextField
import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.components.TopAppBar
@ -109,71 +104,107 @@ private fun RoomDirectoryTopBar(
) )
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun RoomDirectoryContent( private fun RoomDirectoryContent(
state: RoomDirectoryState, state: RoomDirectoryState,
onResultClicked: (RoomId) -> Unit, onResultClicked: (RoomId) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Column( Column(modifier = modifier) {
modifier = modifier, SearchTextField(
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
SearchBar(
query = state.query, query = state.query,
onQueryChange = { query -> onQueryChange = { state.eventSink(RoomDirectoryEvents.Search(it)) },
state.eventSink(RoomDirectoryEvents.Search(query)) placeholder = stringResource(id = CommonStrings.action_search),
}, modifier = Modifier.fillMaxWidth(),
active = state.isSearchActive, )
onActiveChange = { RoomDirectoryRoomList(
state.eventSink(RoomDirectoryEvents.SearchActiveChange(it)) roomDescriptions = state.roomDescriptions,
}, onResultClicked = onResultClicked,
resultState = state.searchResults, )
placeHolderTitle = stringResource(id = CommonStrings.action_search),
) { results ->
RoomDirectoryRoomList(
rooms = results,
onResultClicked = onResultClicked,
)
}
if (!state.isSearchActive) {
RoomDirectoryRoomList(
rooms = state.roomSummaries,
onResultClicked = onResultClicked,
)
}
} }
} }
@Composable @Composable
private fun RoomDirectoryRoomList( private fun RoomDirectoryRoomList(
rooms: ImmutableList<RoomDirectoryRoomSummary>, roomDescriptions: ImmutableList<RoomDescriptionUiModel>,
onResultClicked: (RoomId) -> Unit, onResultClicked: (RoomId) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
LazyColumn( LazyColumn(
modifier = modifier, modifier = modifier,
) { ) {
items(rooms) { room -> items(roomDescriptions) { roomDescription ->
RoomDirectoryRoomRow( RoomDirectoryRoomRow(
room = room, roomDescription = roomDescription,
onClick = onResultClicked, onClick = onResultClicked,
) )
} }
} }
} }
@Composable
private fun SearchTextField(
query: String,
onQueryChange: (String) -> Unit,
placeholder: String,
modifier: Modifier = Modifier,
colors: TextFieldColors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
unfocusedPlaceholderColor = ElementTheme.colors.textPlaceholder,
focusedPlaceholderColor = ElementTheme.colors.textPlaceholder,
focusedTextColor = ElementTheme.colors.textPrimary,
unfocusedTextColor = ElementTheme.colors.textPrimary,
focusedIndicatorColor = ElementTheme.colors.borderInteractiveSecondary,
unfocusedIndicatorColor = ElementTheme.colors.borderInteractiveSecondary,
),
) {
val focusManager = LocalFocusManager.current
TextField(
modifier = modifier,
textStyle = ElementTheme.typography.fontBodyLgRegular,
singleLine = true,
value = query,
onValueChange = onQueryChange,
keyboardActions = KeyboardActions(
onSearch = {
focusManager.clearFocus()
}
),
colors = colors,
placeholder = { Text(placeholder) },
trailingIcon = {
if (query.isNotEmpty()) {
IconButton(
onClick = {
onQueryChange("")
}
) {
Icon(
imageVector = CompoundIcons.Close(),
contentDescription = stringResource(CommonStrings.action_clear),
)
}
} else {
Icon(
imageVector = CompoundIcons.Search(),
contentDescription = stringResource(CommonStrings.action_search),
)
}
},
)
}
@Composable @Composable
private fun RoomDirectoryRoomRow( private fun RoomDirectoryRoomRow(
room: RoomDirectoryRoomSummary, roomDescription: RoomDescriptionUiModel,
onClick: (RoomId) -> Unit, onClick: (RoomId) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Row( Row(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { onClick(room.roomId) } .clickable { onClick(roomDescription.roomId) }
.padding( .padding(
top = 12.dp, top = 12.dp,
bottom = 12.dp, bottom = 12.dp,
@ -182,7 +213,7 @@ private fun RoomDirectoryRoomRow(
.height(IntrinsicSize.Min), .height(IntrinsicSize.Min),
) { ) {
Avatar( Avatar(
avatarData = room.avatarData, avatarData = roomDescription.avatarData,
modifier = Modifier.align(Alignment.CenterVertically) modifier = Modifier.align(Alignment.CenterVertically)
) )
Column( Column(
@ -191,21 +222,21 @@ private fun RoomDirectoryRoomRow(
.padding(start = 16.dp) .padding(start = 16.dp)
) { ) {
Text( Text(
text = room.name, text = roomDescription.name,
maxLines = 1, maxLines = 1,
style = ElementTheme.typography.fontBodyLgRegular, style = ElementTheme.typography.fontBodyLgRegular,
color = ElementTheme.colors.textPrimary, color = ElementTheme.colors.textPrimary,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
Text( Text(
text = room.description, text = roomDescription.description,
maxLines = 1, maxLines = 1,
style = ElementTheme.typography.fontBodyMdRegular, style = ElementTheme.typography.fontBodyMdRegular,
color = ElementTheme.colors.textSecondary, color = ElementTheme.colors.textSecondary,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
} }
if (room.canBeJoined) { if (roomDescription.canBeJoined) {
Text( Text(
text = stringResource(id = CommonStrings.action_join), text = stringResource(id = CommonStrings.action_join),
color = ElementTheme.colors.textSuccessPrimary, color = ElementTheme.colors.textSuccessPrimary,
@ -219,64 +250,6 @@ private fun RoomDirectoryRoomRow(
} }
} }
@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun RoomDirectorySearchTopBar(
query: String,
onQueryChanged: (String) -> Unit,
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
val borderColor = ElementTheme.colors.borderInteractivePrimary
val borderStroke = 1.dp
TopAppBar(
modifier = modifier.drawBehind {
drawLine(
color = borderColor,
start = Offset(0f, size.height),
end = Offset(size.width, size.height),
strokeWidth = borderStroke.value
)
},
title = {
val focusRequester = FocusRequester()
TextField(
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
value = query,
singleLine = true,
onValueChange = onQueryChanged,
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
disabledContainerColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
disabledIndicatorColor = Color.Transparent,
errorIndicatorColor = Color.Transparent,
),
trailingIcon = {
if (query.isNotEmpty()) {
IconButton(onClick = {
onQueryChanged("")
}) {
Icon(
imageVector = CompoundIcons.Close(),
contentDescription = stringResource(CommonStrings.action_cancel),
)
}
}
}
)
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
},
navigationIcon = { BackButton(onClick = onBackPressed) },
)
}
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
fun RoomDirectorySearchViewLightPreview(@PreviewParameter(RoomDirectorySearchStateProvider::class) state: RoomDirectoryState) = ElementPreview { fun RoomDirectorySearchViewLightPreview(@PreviewParameter(RoomDirectorySearchStateProvider::class) state: RoomDirectoryState) = ElementPreview {

View file

@ -1,81 +0,0 @@
/*
* 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.datasource
import io.element.android.features.roomdirectory.impl.root.model.RoomDirectoryRoomSummary
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 kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOf
import javax.inject.Inject
class RoomDirectoryDataSource @Inject constructor() {
private val _searchResults = MutableStateFlow<ImmutableList<RoomDirectoryRoomSummary>>(persistentListOf())
suspend fun updateSearchQuery(searchQuery: String) {
//TODO branch to matrix sdk
if (searchQuery.isEmpty()) {
_searchResults.value = persistentListOf()
} else {
delay(100)
_searchResults.value = all.value.filter {
it.name.contains(searchQuery)
}.toImmutableList()
}
}
suspend fun loadMore() {
//TODO branch to matrix sdk
}
val all: StateFlow<ImmutableList<RoomDirectoryRoomSummary>> = MutableStateFlow(
persistentListOf(
RoomDirectoryRoomSummary(
roomId = RoomId("!exa:matrix.org"),
name = "Element X Android",
description = "Element X is a secure, private and decentralized messenger.",
avatarData = AvatarData(
id = "!exa:matrix.org",
name = "Element X Android",
url = null,
size = AvatarSize.RoomDirectorySearchItem
),
canBeJoined = true,
),
RoomDirectoryRoomSummary(
roomId = RoomId("!exi:matrix.org"),
name = "Element X iOS",
description = "Element X is a secure, private and decentralized messenger.",
avatarData = AvatarData(
id = "!exi:matrix.org",
name = "Element X iOS",
url = null,
size = AvatarSize.RoomDirectorySearchItem
),
canBeJoined = false,
)
)
)
val searchResults: StateFlow<ImmutableList<RoomDirectoryRoomSummary>> = _searchResults
}

View file

@ -17,12 +17,29 @@
package io.element.android.features.roomdirectory.impl.root.model package io.element.android.features.roomdirectory.impl.root.model
import io.element.android.libraries.designsystem.components.avatar.AvatarData 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.core.RoomId
import io.element.android.libraries.matrix.api.roomdirectory.RoomDescription
data class RoomDirectoryRoomSummary( data class RoomDescriptionUiModel(
val roomId: RoomId, val roomId: RoomId,
val name: String, val name: String,
val description: String, val description: String,
val avatarData: AvatarData, val avatarData: AvatarData,
val canBeJoined: Boolean, val canBeJoined: Boolean,
) )
fun RoomDescription.toUiModel(): RoomDescriptionUiModel {
return RoomDescriptionUiModel(
roomId = roomId,
name = name ?: "",
description = topic ?: "",
avatarData = AvatarData(
id = roomId.value,
name = name ?: "",
url = avatarUrl,
size = AvatarSize.RoomDirectoryItem,
),
canBeJoined = joinRule == RoomDescription.JoinRule.PUBLIC,
)
}

View file

@ -53,5 +53,5 @@ enum class AvatarSize(val dp: Dp) {
CustomRoomNotificationSetting(36.dp), CustomRoomNotificationSetting(36.dp),
RoomDirectorySearchItem(36.dp), RoomDirectoryItem(36.dp),
} }

View file

@ -16,11 +16,11 @@
package io.element.android.libraries.matrix.api.roomdirectory package io.element.android.libraries.matrix.api.roomdirectory
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.Flow
interface RoomDirectorySearch { interface RoomDirectoryList {
suspend fun updateQuery(query: String?, batchSize: Int) suspend fun filter(filter: String?, batchSize: Int)
suspend fun loadMore() suspend fun loadMore()
suspend fun hasMoreToLoad(): Boolean suspend fun hasMoreToLoad(): Boolean
val results: SharedFlow<List<RoomDescription>> val items: Flow<List<RoomDescription>>
} }

View file

@ -17,5 +17,5 @@
package io.element.android.libraries.matrix.api.roomdirectory package io.element.android.libraries.matrix.api.roomdirectory
interface RoomDirectoryService { interface RoomDirectoryService {
fun search(): RoomDirectorySearch fun createRoomDirectoryList(): RoomDirectoryList
} }

View file

@ -26,6 +26,7 @@ import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -68,4 +69,9 @@ object SessionMatrixModule {
fun provideSessionCoroutineScope(matrixClient: MatrixClient): CoroutineScope { fun provideSessionCoroutineScope(matrixClient: MatrixClient): CoroutineScope {
return matrixClient.sessionCoroutineScope return matrixClient.sessionCoroutineScope
} }
@Provides
fun providesRoomDirectoryService(matrixClient: MatrixClient): RoomDirectoryService {
return matrixClient.roomDirectoryService()
}
} }

View file

@ -17,26 +17,28 @@
package io.element.android.libraries.matrix.impl.roomdirectory 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.RoomDirectorySearch import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.milliseconds
import org.matrix.rustcomponents.sdk.RoomDirectorySearch as InnerRoomDirectorySearch import org.matrix.rustcomponents.sdk.RoomDirectorySearch as InnerRoomDirectorySearch
class RustRoomDirectorySearch( class RustRoomDirectoryList(
private val inner: InnerRoomDirectorySearch, private val inner: InnerRoomDirectorySearch,
private val sessionCoroutineScope: CoroutineScope, private val sessionCoroutineScope: CoroutineScope,
private val sessionDispatcher: CoroutineDispatcher, private val sessionDispatcher: CoroutineDispatcher,
) : RoomDirectorySearch { ) : RoomDirectoryList {
private val _results: MutableStateFlow<List<RoomDescription>> = private val _items = MutableSharedFlow<List<RoomDescription>>()
MutableStateFlow(emptyList())
private val processor = RoomDirectorySearchProcessor(_results, sessionDispatcher, RoomDescriptionMapper()) private val processor = RoomDirectorySearchProcessor(_items, sessionDispatcher, RoomDescriptionMapper())
init { init {
sessionCoroutineScope.launch(sessionDispatcher) { sessionCoroutineScope.launch(sessionDispatcher) {
@ -49,8 +51,8 @@ class RustRoomDirectorySearch(
} }
} }
override suspend fun updateQuery(query: String?, batchSize: Int) { override suspend fun filter(filter: String?, batchSize: Int) {
inner.search(query, batchSize.toUInt()) inner.search(filter, batchSize.toUInt())
} }
override suspend fun loadMore() { override suspend fun loadMore() {
@ -61,5 +63,6 @@ class RustRoomDirectorySearch(
return !inner.isAtLastPage() return !inner.isAtLastPage()
} }
override val results: SharedFlow<List<RoomDescription>> = _results @OptIn(FlowPreview::class)
override val items: Flow<List<RoomDescription>> = _items.debounce(200.milliseconds)
} }

View file

@ -16,7 +16,7 @@
package io.element.android.libraries.matrix.impl.roomdirectory package io.element.android.libraries.matrix.impl.roomdirectory
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectorySearch 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.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -28,8 +28,8 @@ class RustRoomDirectoryService(
private val sessionDispatcher: CoroutineDispatcher, private val sessionDispatcher: CoroutineDispatcher,
) : RoomDirectoryService { ) : RoomDirectoryService {
override fun search(): RoomDirectorySearch { override fun createRoomDirectoryList(): RoomDirectoryList {
return RustRoomDirectorySearch(client.roomDirectorySearch(), sessionCoroutineScope, sessionDispatcher) return RustRoomDirectoryList(client.roomDirectorySearch(), sessionCoroutineScope, sessionDispatcher)
} }
} }

View file

@ -16,11 +16,11 @@
package io.element.android.libraries.matrix.test.roomdirectory package io.element.android.libraries.matrix.test.roomdirectory
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectorySearch 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
class FakeRoomDirectoryService : RoomDirectoryService { class FakeRoomDirectoryService : RoomDirectoryService {
override fun search(): RoomDirectorySearch { override fun createRoomDirectoryList(): RoomDirectoryList {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
} }