Room directory search : start implementing ui with fake data

This commit is contained in:
ganfra
2024-03-20 18:32:41 +01:00
parent 3de4e8c91e
commit fcfe4e9d31
9 changed files with 401 additions and 15 deletions

View File

@@ -40,6 +40,7 @@ dependencies {
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.uiStrings)
testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test)

View File

@@ -38,6 +38,7 @@ class RoomDirectorySearchNode @AssistedInject constructor(
val state = presenter.present()
RoomDirectorySearchView(
state = state,
onBackPressed = ::navigateUp,
modifier = modifier
)
}

View File

@@ -17,24 +17,65 @@
package io.element.android.features.roomdirectory.impl.search
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import io.element.android.features.roomdirectory.impl.search.datasource.RoomDirectorySearchDataSource
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
class RoomDirectorySearchPresenter @Inject constructor() : Presenter<RoomDirectorySearchState> {
class RoomDirectorySearchPresenter @Inject constructor(
private val client: MatrixClient,
private val dataSource: RoomDirectorySearchDataSource,
) : Presenter<RoomDirectorySearchState> {
@Composable
override fun present(): RoomDirectorySearchState {
var searchQuery by rememberSaveable {
mutableStateOf("")
}
val results by dataSource.searchResults.collectAsState()
val coroutineScope = rememberCoroutineScope()
LaunchedEffect(searchQuery) {
dataSource.updateSearchQuery(searchQuery)
}
fun handleEvents(event: RoomDirectorySearchEvents) {
when (event) {
is RoomDirectorySearchEvents.JoinRoom -> TODO()
RoomDirectorySearchEvents.LoadMore -> TODO()
is RoomDirectorySearchEvents.Search -> TODO()
is RoomDirectorySearchEvents.JoinRoom -> {
coroutineScope.joinRoom(event.roomId)
}
RoomDirectorySearchEvents.LoadMore -> {
coroutineScope.launch {
dataSource.loadMore()
}
}
is RoomDirectorySearchEvents.Search -> {
searchQuery = event.query
}
}
}
return RoomDirectorySearchState(
query = searchQuery,
results = results,
eventSink = ::handleEvents
)
}
private fun CoroutineScope.joinRoom(roomId: RoomId) = launch {
client.getRoom(roomId)?.join()
}
}

View File

@@ -16,6 +16,11 @@
package io.element.android.features.roomdirectory.impl.search
import io.element.android.features.roomdirectory.impl.search.model.RoomDirectorySearchResult
import kotlinx.collections.immutable.ImmutableList
data class RoomDirectorySearchState(
val query: String,
val results: ImmutableList<RoomDirectorySearchResult>,
val eventSink: (RoomDirectorySearchEvents) -> Unit
)

View File

@@ -17,15 +17,54 @@
package io.element.android.features.roomdirectory.impl.search
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.roomdirectory.impl.search.model.RoomDirectorySearchResult
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
open class RoomDirectorySearchStateProvider : PreviewParameterProvider<RoomDirectorySearchState> {
override val values: Sequence<RoomDirectorySearchState>
get() = sequenceOf(
aRoomDirectorySearchState(),
// Add other states here
aRoomDirectorySearchState(
query = "Element",
results = persistentListOf(
RoomDirectorySearchResult(
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,
),
RoomDirectorySearchResult(
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,
)
)
),
)
}
fun aRoomDirectorySearchState() = RoomDirectorySearchState(
eventSink = {}
fun aRoomDirectorySearchState(
query: String = "",
results: ImmutableList<RoomDirectorySearchResult> = persistentListOf(),
) = RoomDirectorySearchState(
query = query,
results = results,
eventSink = { }
)

View File

@@ -16,27 +16,215 @@
package io.element.android.features.roomdirectory.impl.search
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.MaterialTheme
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
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.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
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.search.model.RoomDirectorySearchResult
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
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.Text
import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun RoomDirectorySearchView(
state: RoomDirectorySearchState,
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
Box(modifier, contentAlignment = Alignment.Center) {
Text(
"RoomDirectorySearch feature view",
color = MaterialTheme.colorScheme.primary,
)
fun onQueryChanged(query: String) {
state.eventSink(RoomDirectorySearchEvents.Search(query))
}
Scaffold(
modifier = modifier,
topBar = {
RoomDirectorySearchTopBar(
query = state.query,
onQueryChanged = ::onQueryChanged,
onBackPressed = onBackPressed,
)
},
content = { padding ->
RoomDirectorySearchContent(
state = state,
onResultClicked = { roomId ->
state.eventSink(RoomDirectorySearchEvents.JoinRoom(roomId))
},
modifier = Modifier
.padding(padding)
.consumeWindowInsets(padding)
)
}
)
}
@Composable
private fun RoomDirectorySearchContent(
state: RoomDirectorySearchState,
onResultClicked: (RoomId) -> Unit,
modifier: Modifier = Modifier,
) {
LazyColumn(modifier = modifier) {
items(state.results) { result ->
RoomDirectorySearchResultRow(
result = result,
onClick = onResultClicked,
)
}
}
}
@Composable
private fun RoomDirectorySearchResultRow(
result: RoomDirectorySearchResult,
onClick: (RoomId) -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.fillMaxWidth()
.clickable { onClick(result.roomId) }
.padding(
top = 12.dp,
bottom = 12.dp,
start = 16.dp,
)
.height(IntrinsicSize.Min),
) {
Avatar(
avatarData = result.avatarData,
modifier = Modifier.align(Alignment.CenterVertically)
)
Column(
modifier = Modifier
.weight(1f)
.padding(start = 16.dp)
) {
Text(
text = result.name,
maxLines = 1,
style = ElementTheme.typography.fontBodyLgRegular,
color = ElementTheme.colors.textPrimary,
overflow = TextOverflow.Ellipsis,
)
Text(
text = result.description,
maxLines = 1,
style = ElementTheme.typography.fontBodyMdRegular,
color = ElementTheme.colors.textSecondary,
overflow = TextOverflow.Ellipsis,
)
}
if (result.canBeJoined) {
CompositionLocalProvider(LocalContentColor provides ElementTheme.colors.textSuccessPrimary) {
TextButton(
text = stringResource(id = CommonStrings.action_join),
onClick = { onClick(result.roomId) },
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(start = 4.dp, end = 12.dp)
)
}
} else {
Spacer(modifier = Modifier.width(24.dp))
}
}
}
@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
@@ -44,5 +232,6 @@ fun RoomDirectorySearchView(
fun RoomDirectorySearchViewLightPreview(@PreviewParameter(RoomDirectorySearchStateProvider::class) state: RoomDirectorySearchState) = ElementPreview {
RoomDirectorySearchView(
state = state,
onBackPressed = {},
)
}

View File

@@ -0,0 +1,80 @@
/*
* 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.search.datasource
import io.element.android.features.roomdirectory.impl.search.model.RoomDirectorySearchResult
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.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
class RoomDirectorySearchDataSource @Inject constructor(
) {
private val _searchResults = MutableStateFlow<ImmutableList<RoomDirectorySearchResult>>(persistentListOf())
suspend fun updateSearchQuery(searchQuery: String) {
//TODO branch to matrix sdk
if (searchQuery.isEmpty()) {
_searchResults.value = persistentListOf()
} else {
delay(100)
emitFakeResults()
}
}
suspend fun loadMore() {
//TODO branch to matrix sdk
}
private fun emitFakeResults() {
_searchResults.value = persistentListOf(
RoomDirectorySearchResult(
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,
),
RoomDirectorySearchResult(
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<RoomDirectorySearchResult>> = _searchResults
}

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.impl.search.model
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.api.core.RoomId
data class RoomDirectorySearchResult(
val roomId: RoomId,
val name: String,
val description: String,
val avatarData: AvatarData,
val canBeJoined: Boolean,
)

View File

@@ -51,5 +51,7 @@ enum class AvatarSize(val dp: Dp) {
NotificationsOptIn(32.dp),
CustomRoomNotificationSetting(36.dp)
CustomRoomNotificationSetting(36.dp),
RoomDirectorySearchItem(36.dp),
}