forked from dsutanto/bChot-android
First Commit
This commit is contained in:
42
features/roomdirectory/impl/build.gradle.kts
Normal file
42
features/roomdirectory/impl/build.gradle.kts
Normal file
@@ -0,0 +1,42 @@
|
||||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
id("kotlin-parcelize")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.roomdirectory.impl"
|
||||
testOptions {
|
||||
unitTests {
|
||||
isIncludeAndroidResources = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setupDependencyInjection()
|
||||
|
||||
dependencies {
|
||||
api(projects.features.roomdirectory.api)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.testtags)
|
||||
implementation(projects.services.analytics.api)
|
||||
|
||||
testCommonDependencies(libs, true)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomdirectory.impl
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint
|
||||
import io.element.android.features.roomdirectory.impl.root.RoomDirectoryNode
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultRoomDirectoryEntryPoint : RoomDirectoryEntryPoint {
|
||||
override fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
callback: RoomDirectoryEntryPoint.Callback,
|
||||
): Node {
|
||||
return parentNode.createNode<RoomDirectoryNode>(buildContext, listOf(callback))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomdirectory.impl.root
|
||||
|
||||
sealed interface RoomDirectoryEvents {
|
||||
data class Search(val query: String) : RoomDirectoryEvents
|
||||
data object LoadMore : RoomDirectoryEvents
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomdirectory.impl.root
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@AssistedInject
|
||||
class RoomDirectoryNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: RoomDirectoryPresenter,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
private val callback: RoomDirectoryEntryPoint.Callback = callback()
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
RoomDirectoryView(
|
||||
state = state,
|
||||
onResultClick = callback::navigateToRoom,
|
||||
onBackClick = ::navigateUp,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomdirectory.impl.root
|
||||
|
||||
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.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Inject
|
||||
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.Presenter
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList
|
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
private const val SEARCH_BATCH_SIZE = 20
|
||||
|
||||
@Inject
|
||||
class RoomDirectoryPresenter(
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
private val roomDirectoryService: RoomDirectoryService,
|
||||
) : Presenter<RoomDirectoryState> {
|
||||
@Composable
|
||||
override fun present(): RoomDirectoryState {
|
||||
var loadingMore by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
var searchQuery by rememberSaveable {
|
||||
mutableStateOf<String?>(null)
|
||||
}
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val roomDirectoryList = remember {
|
||||
roomDirectoryService.createRoomDirectoryList(coroutineScope)
|
||||
}
|
||||
val listState by roomDirectoryList.collectState()
|
||||
LaunchedEffect(searchQuery) {
|
||||
if (searchQuery == null) return@LaunchedEffect
|
||||
// cancel load more right away
|
||||
loadingMore = false
|
||||
// debounce search query
|
||||
delay(300)
|
||||
roomDirectoryList.filter(filter = searchQuery, batchSize = SEARCH_BATCH_SIZE, viaServerName = null)
|
||||
}
|
||||
LaunchedEffect(loadingMore) {
|
||||
if (loadingMore) {
|
||||
roomDirectoryList.loadMore()
|
||||
loadingMore = false
|
||||
}
|
||||
}
|
||||
fun handleEvent(event: RoomDirectoryEvents) {
|
||||
when (event) {
|
||||
RoomDirectoryEvents.LoadMore -> {
|
||||
loadingMore = true
|
||||
}
|
||||
is RoomDirectoryEvents.Search -> {
|
||||
searchQuery = event.query
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return RoomDirectoryState(
|
||||
query = searchQuery.orEmpty(),
|
||||
roomDescriptions = listState.items,
|
||||
displayLoadMoreIndicator = listState.hasMoreToLoad,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
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(RoomDirectoryListState.Default)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomdirectory.impl.root
|
||||
|
||||
import io.element.android.features.roomdirectory.api.RoomDescription
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class RoomDirectoryState(
|
||||
val query: String,
|
||||
val roomDescriptions: ImmutableList<RoomDescription>,
|
||||
val displayLoadMoreIndicator: Boolean,
|
||||
val eventSink: (RoomDirectoryEvents) -> Unit
|
||||
) {
|
||||
val displayEmptyState = roomDescriptions.isEmpty() && !displayLoadMoreIndicator
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomdirectory.impl.root
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.roomdirectory.api.RoomDescription
|
||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
open class RoomDirectoryStateProvider : PreviewParameterProvider<RoomDirectoryState> {
|
||||
override val values: Sequence<RoomDirectoryState>
|
||||
get() = sequenceOf(
|
||||
aRoomDirectoryState(),
|
||||
aRoomDirectoryState(
|
||||
query = "Element",
|
||||
roomDescriptions = aRoomDescriptionList(),
|
||||
),
|
||||
aRoomDirectoryState(
|
||||
query = "Element",
|
||||
roomDescriptions = aRoomDescriptionList(),
|
||||
displayLoadMoreIndicator = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun aRoomDirectoryState(
|
||||
query: String = "",
|
||||
displayLoadMoreIndicator: Boolean = false,
|
||||
roomDescriptions: ImmutableList<RoomDescription> = persistentListOf(),
|
||||
eventSink: (RoomDirectoryEvents) -> Unit = {},
|
||||
) = RoomDirectoryState(
|
||||
query = query,
|
||||
roomDescriptions = roomDescriptions,
|
||||
displayLoadMoreIndicator = displayLoadMoreIndicator,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
fun aRoomDescriptionList(): ImmutableList<RoomDescription> {
|
||||
return persistentListOf(
|
||||
RoomDescription(
|
||||
roomId = RoomId("!exa:matrix.org"),
|
||||
name = "Element X Android",
|
||||
topic = "Element X is a secure, private and decentralized messenger.",
|
||||
alias = RoomAlias("#element-x-android:matrix.org"),
|
||||
avatarUrl = null,
|
||||
joinRule = RoomDescription.JoinRule.PUBLIC,
|
||||
numberOfMembers = 2765,
|
||||
),
|
||||
RoomDescription(
|
||||
roomId = RoomId("!exi:matrix.org"),
|
||||
name = "Element X iOS",
|
||||
topic = "Element X is a secure, private and decentralized messenger.",
|
||||
alias = RoomAlias("#element-x-ios:matrix.org"),
|
||||
avatarUrl = null,
|
||||
joinRule = RoomDescription.JoinRule.UNKNOWN,
|
||||
numberOfMembers = 356,
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomdirectory.impl.root
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.wrapContentHeight
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.TextFieldColors
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.testTag
|
||||
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.api.RoomDescription
|
||||
import io.element.android.features.roomdirectory.impl.R
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarType
|
||||
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.CircularProgressIndicator
|
||||
import io.element.android.libraries.designsystem.theme.components.FilledTextField
|
||||
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.TopAppBar
|
||||
import io.element.android.libraries.testtags.TestTags
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
fun RoomDirectoryView(
|
||||
state: RoomDirectoryState,
|
||||
onResultClick: (RoomDescription) -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
RoomDirectoryTopBar(onBackClick = onBackClick)
|
||||
},
|
||||
content = { padding ->
|
||||
RoomDirectoryContent(
|
||||
state = state,
|
||||
onResultClick = onResultClick,
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun RoomDirectoryTopBar(
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
TopAppBar(
|
||||
modifier = modifier,
|
||||
navigationIcon = {
|
||||
BackButton(onClick = onBackClick)
|
||||
},
|
||||
titleStr = stringResource(id = R.string.screen_room_directory_search_title),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RoomDirectoryContent(
|
||||
state: RoomDirectoryState,
|
||||
onResultClick: (RoomDescription) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
SearchTextField(
|
||||
query = state.query,
|
||||
onQueryChange = { state.eventSink(RoomDirectoryEvents.Search(it)) },
|
||||
placeholder = stringResource(id = CommonStrings.action_search),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
RoomDirectoryRoomList(
|
||||
roomDescriptions = state.roomDescriptions,
|
||||
displayLoadMoreIndicator = state.displayLoadMoreIndicator,
|
||||
displayEmptyState = state.displayEmptyState,
|
||||
onResultClick = onResultClick,
|
||||
onReachedLoadMore = { state.eventSink(RoomDirectoryEvents.LoadMore) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RoomDirectoryRoomList(
|
||||
roomDescriptions: ImmutableList<RoomDescription>,
|
||||
displayLoadMoreIndicator: Boolean,
|
||||
displayEmptyState: Boolean,
|
||||
onResultClick: (RoomDescription) -> Unit,
|
||||
onReachedLoadMore: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyColumn(modifier = modifier) {
|
||||
items(roomDescriptions) { roomDescription ->
|
||||
RoomDirectoryRoomRow(
|
||||
roomDescription = roomDescription,
|
||||
onClick = {
|
||||
onResultClick(roomDescription)
|
||||
},
|
||||
)
|
||||
}
|
||||
if (displayEmptyState) {
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(id = CommonStrings.common_no_results),
|
||||
style = ElementTheme.typography.fontBodyLgRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
if (displayLoadMoreIndicator) {
|
||||
item {
|
||||
LoadMoreIndicator(modifier = Modifier.fillMaxWidth())
|
||||
LaunchedEffect(onReachedLoadMore) {
|
||||
onReachedLoadMore()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadMoreIndicator(modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(24.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
strokeWidth = 2.dp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@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.textSecondary,
|
||||
focusedPlaceholderColor = ElementTheme.colors.textSecondary,
|
||||
focusedTextColor = ElementTheme.colors.textPrimary,
|
||||
unfocusedTextColor = ElementTheme.colors.textPrimary,
|
||||
focusedIndicatorColor = ElementTheme.colors.borderInteractiveSecondary,
|
||||
unfocusedIndicatorColor = ElementTheme.colors.borderInteractiveSecondary,
|
||||
),
|
||||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
FilledTextField(
|
||||
modifier = modifier.testTag(TestTags.searchTextField.value),
|
||||
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
|
||||
private fun RoomDirectoryRoomRow(
|
||||
roomDescription: RoomDescription,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.padding(
|
||||
top = 12.dp,
|
||||
bottom = 12.dp,
|
||||
start = 16.dp,
|
||||
)
|
||||
.height(IntrinsicSize.Min),
|
||||
) {
|
||||
Avatar(
|
||||
avatarData = roomDescription.avatarData(AvatarSize.RoomDirectoryItem),
|
||||
avatarType = AvatarType.Room(),
|
||||
modifier = Modifier.align(Alignment.CenterVertically),
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = roomDescription.computedName,
|
||||
maxLines = 1,
|
||||
style = ElementTheme.typography.fontBodyLgRegular,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text(
|
||||
text = roomDescription.computedDescription,
|
||||
maxLines = 1,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun RoomDirectoryViewPreview(@PreviewParameter(RoomDirectoryStateProvider::class) state: RoomDirectoryState) = ElementPreview {
|
||||
RoomDirectoryView(
|
||||
state = state,
|
||||
onResultClick = {},
|
||||
onBackClick = {},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomdirectory.impl.root.model
|
||||
|
||||
import io.element.android.features.roomdirectory.api.RoomDescription
|
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomDescription as MatrixRoomDescription
|
||||
|
||||
fun MatrixRoomDescription.toFeatureModel(): RoomDescription {
|
||||
return RoomDescription(
|
||||
roomId = roomId,
|
||||
name = name,
|
||||
alias = alias,
|
||||
topic = topic,
|
||||
avatarUrl = avatarUrl,
|
||||
numberOfMembers = numberOfMembers,
|
||||
joinRule = when (joinRule) {
|
||||
MatrixRoomDescription.JoinRule.PUBLIC -> RoomDescription.JoinRule.PUBLIC
|
||||
MatrixRoomDescription.JoinRule.KNOCK -> RoomDescription.JoinRule.KNOCK
|
||||
MatrixRoomDescription.JoinRule.RESTRICTED -> RoomDescription.JoinRule.RESTRICTED
|
||||
MatrixRoomDescription.JoinRule.KNOCK_RESTRICTED -> RoomDescription.JoinRule.KNOCK_RESTRICTED
|
||||
MatrixRoomDescription.JoinRule.INVITE -> RoomDescription.JoinRule.INVITE
|
||||
MatrixRoomDescription.JoinRule.UNKNOWN -> RoomDescription.JoinRule.UNKNOWN
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
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()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_directory_search_loading_error">"Памылка загрузкі"</string>
|
||||
<string name="screen_room_directory_search_title">"Каталог пакояў"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_directory_search_loading_error">"Načítání se nezdařilo"</string>
|
||||
<string name="screen_room_directory_search_title">"Adresář místností"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_directory_search_loading_error">"Wedi methu llwytho"</string>
|
||||
<string name="screen_room_directory_search_title">"Cyfeiriadur ystafelloedd"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_directory_search_loading_error">"Indlæsning mislykkedes"</string>
|
||||
<string name="screen_room_directory_search_title">"Register over rum"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_directory_search_loading_error">"Fehler beim Laden"</string>
|
||||
<string name="screen_room_directory_search_title">"Chat-Verzeichnis"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_directory_search_loading_error">"Αποτυχία φόρτωσης"</string>
|
||||
<string name="screen_room_directory_search_title">"Κατάλογος αιθουσών"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_directory_search_loading_error">"Carga fallida"</string>
|
||||
<string name="screen_room_directory_search_title">"Directorio de salas"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_directory_search_loading_error">"Andmeid ei õnnestunud laadida"</string>
|
||||
<string name="screen_room_directory_search_title">"Jututubade kataloog"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_directory_search_loading_error">"Ezin izan da kargatu"</string>
|
||||
<string name="screen_room_directory_search_title">"Gelen direktorioa"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_directory_search_loading_error">"شکست در بار کردن"</string>
|
||||
<string name="screen_room_directory_search_title">"فهرست اتاقها"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_directory_search_loading_error">"Lataus epäonnistui"</string>
|
||||
<string name="screen_room_directory_search_title">"Huoneluettelo"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_directory_search_loading_error">"Échec du chargement"</string>
|
||||
<string name="screen_room_directory_search_title">"Annuaire des salons"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_directory_search_loading_error">"Sikertelen betöltés"</string>
|
||||
<string name="screen_room_directory_search_title">"Szobakatalógus"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_directory_search_loading_error">"Gagal memuat"</string>
|
||||
<string name="screen_room_directory_search_title">"Direktori ruangan"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_directory_search_loading_error">"Caricamento fallito"</string>
|
||||
<string name="screen_room_directory_search_title">"Elenco delle stanze"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_directory_search_loading_error">"ჩატვირთვა წარუმატებელია"</string>
|
||||
<string name="screen_room_directory_search_title">"ოთახის კატალოგი"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_directory_search_loading_error">"로드에 실패했습니다"</string>
|
||||
<string name="screen_room_directory_search_title">"방 디렉토리"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_directory_search_loading_error">"Kunne ikke laste inn"</string>
|
||||
<string name="screen_room_directory_search_title">"Romkatalog"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_directory_search_loading_error">"Laden mislukt"</string>
|
||||
<string name="screen_room_directory_search_title">"Kamergids"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_directory_search_loading_error">"Błąd wczytywania"</string>
|
||||
<string name="screen_room_directory_search_title">"Katalog pokoi"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_directory_search_loading_error">"Falha no carregamento"</string>
|
||||
<string name="screen_room_directory_search_title">"Diretório de salas"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_directory_search_loading_error">"Falha ao carregar"</string>
|
||||
<string name="screen_room_directory_search_title">"Diretório de salas"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_directory_search_loading_error">"Încărcare eșuată"</string>
|
||||
<string name="screen_room_directory_search_title">"Director de camere"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_directory_search_loading_error">"Сбой загрузки"</string>
|
||||
<string name="screen_room_directory_search_title">"Каталог комнат"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_directory_search_loading_error">"Načítanie zlyhalo"</string>
|
||||
<string name="screen_room_directory_search_title">"Adresár miestností"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_directory_search_loading_error">"Misslyckades att ladda"</string>
|
||||
<string name="screen_room_directory_search_title">"Rumskatalog"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_directory_search_loading_error">"Yükleme başarısız"</string>
|
||||
<string name="screen_room_directory_search_title">"Oda dizini"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_directory_search_loading_error">"Не вдалося завантажити"</string>
|
||||
<string name="screen_room_directory_search_title">"Каталог кімнат"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_directory_search_loading_error">"لادنا ناکام"</string>
|
||||
<string name="screen_room_directory_search_title">"کمرے کا راہنامچہ"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_directory_search_loading_error">"Yuklab bo‘lmadi"</string>
|
||||
<string name="screen_room_directory_search_title">"Xona katalogi"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_directory_search_loading_error">"無法載入"</string>
|
||||
<string name="screen_room_directory_search_title">"聊天室目錄"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_directory_search_loading_error">"加载失败"</string>
|
||||
<string name="screen_room_directory_search_title">"聊天室目录"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_directory_search_loading_error">"Failed loading"</string>
|
||||
<string name="screen_room_directory_search_title">"Room directory"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomdirectory.impl
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.roomdirectory.api.RoomDescription
|
||||
import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint
|
||||
import io.element.android.features.roomdirectory.impl.root.RoomDirectoryNode
|
||||
import io.element.android.features.roomdirectory.impl.root.createRoomDirectoryPresenter
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.node.TestParentNode
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultRoomDirectoryEntryPointTest {
|
||||
@get:Rule
|
||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
@Test
|
||||
fun `test node builder`() = runTest {
|
||||
val entryPoint = DefaultRoomDirectoryEntryPoint()
|
||||
val parentNode = TestParentNode.create { buildContext, plugins ->
|
||||
RoomDirectoryNode(
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
presenter = createRoomDirectoryPresenter(),
|
||||
)
|
||||
}
|
||||
val callback = object : RoomDirectoryEntryPoint.Callback {
|
||||
override fun navigateToRoom(roomDescription: RoomDescription) = lambdaError()
|
||||
}
|
||||
val result = entryPoint.createNode(
|
||||
parentNode = parentNode,
|
||||
buildContext = BuildContext.root(null),
|
||||
callback = callback,
|
||||
)
|
||||
assertThat(result).isInstanceOf(RoomDirectoryNode::class.java)
|
||||
assertThat(result.plugins).contains(callback)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomdirectory.impl.root
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList
|
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
|
||||
import io.element.android.libraries.matrix.test.roomdirectory.FakeRoomDirectoryList
|
||||
import io.element.android.libraries.matrix.test.roomdirectory.FakeRoomDirectoryService
|
||||
import io.element.android.libraries.matrix.test.roomdirectory.aRoomDescription
|
||||
import io.element.android.tests.testutils.lambda.any
|
||||
import io.element.android.tests.testutils.lambda.assert
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import io.element.android.tests.testutils.test
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class RoomDirectoryPresenterTest {
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = createRoomDirectoryPresenter()
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.query).isEmpty()
|
||||
assertThat(initialState.displayEmptyState).isFalse()
|
||||
assertThat(initialState.roomDescriptions).isEmpty()
|
||||
assertThat(initialState.displayLoadMoreIndicator).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - room directory list emits empty state`() = runTest {
|
||||
val directoryListStateFlow = MutableSharedFlow<RoomDirectoryList.SearchResult>(replay = 1)
|
||||
val roomDirectoryList = FakeRoomDirectoryList(directoryListStateFlow)
|
||||
val roomDirectoryService = FakeRoomDirectoryService { roomDirectoryList }
|
||||
val presenter = createRoomDirectoryPresenter(roomDirectoryService = roomDirectoryService)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
directoryListStateFlow.emit(
|
||||
RoomDirectoryList.SearchResult(false, emptyList())
|
||||
)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.displayEmptyState).isTrue()
|
||||
}
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - room directory list emits non-empty state`() = runTest {
|
||||
val directoryListStateFlow = MutableSharedFlow<RoomDirectoryList.SearchResult>(replay = 1)
|
||||
val roomDirectoryList = FakeRoomDirectoryList(directoryListStateFlow)
|
||||
val roomDirectoryService = FakeRoomDirectoryService { roomDirectoryList }
|
||||
val presenter = createRoomDirectoryPresenter(roomDirectoryService = roomDirectoryService)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
directoryListStateFlow.emit(
|
||||
RoomDirectoryList.SearchResult(
|
||||
hasMoreToLoad = true,
|
||||
items = listOf(aRoomDescription())
|
||||
)
|
||||
)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.displayEmptyState).isFalse()
|
||||
assertThat(state.roomDescriptions).hasSize(1)
|
||||
}
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - emit search event`() = runTest {
|
||||
val filterLambda = lambdaRecorder { _: String?, _: Int, _: String? ->
|
||||
Result.success(Unit)
|
||||
}
|
||||
val roomDirectoryList = FakeRoomDirectoryList(filterLambda = filterLambda)
|
||||
val roomDirectoryService = FakeRoomDirectoryService { roomDirectoryList }
|
||||
val presenter = createRoomDirectoryPresenter(roomDirectoryService = roomDirectoryService)
|
||||
presenter.test {
|
||||
awaitItem().also { state ->
|
||||
state.eventSink(RoomDirectoryEvents.Search("test"))
|
||||
}
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.query).isEqualTo("test")
|
||||
}
|
||||
advanceUntilIdle()
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
assert(filterLambda)
|
||||
.isCalledOnce()
|
||||
.with(value("test"), any(), value(null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - emit load more event`() = runTest {
|
||||
val loadMoreLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
|
||||
val roomDirectoryList = FakeRoomDirectoryList(loadMoreLambda = loadMoreLambda)
|
||||
val roomDirectoryService = FakeRoomDirectoryService { roomDirectoryList }
|
||||
val presenter = createRoomDirectoryPresenter(roomDirectoryService = roomDirectoryService)
|
||||
presenter.test {
|
||||
awaitItem().also { state ->
|
||||
state.eventSink(RoomDirectoryEvents.LoadMore)
|
||||
}
|
||||
advanceUntilIdle()
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
assert(loadMoreLambda)
|
||||
.isCalledOnce()
|
||||
.withNoParameter()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun TestScope.createRoomDirectoryPresenter(
|
||||
roomDirectoryService: RoomDirectoryService = FakeRoomDirectoryService(
|
||||
createRoomDirectoryListFactory = { FakeRoomDirectoryList() }
|
||||
),
|
||||
): RoomDirectoryPresenter {
|
||||
return RoomDirectoryPresenter(
|
||||
dispatchers = testCoroutineDispatchers(),
|
||||
roomDirectoryService = roomDirectoryService,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomdirectory.impl.root
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.features.roomdirectory.api.RoomDescription
|
||||
import io.element.android.libraries.testtags.TestTags
|
||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
||||
import io.element.android.tests.testutils.EventsRecorder
|
||||
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class RoomDirectoryViewTest {
|
||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `typing text in search field emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<RoomDirectoryEvents>()
|
||||
rule.setRoomDirectoryView(
|
||||
state = aRoomDirectoryState(
|
||||
eventSink = eventsRecorder,
|
||||
)
|
||||
)
|
||||
rule.onNodeWithTag(TestTags.searchTextField.value).performTextInput(
|
||||
text = "Test"
|
||||
)
|
||||
eventsRecorder.assertSingle(RoomDirectoryEvents.Search("Test"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on room item then onResultClick lambda is called once`() {
|
||||
val eventsRecorder = EventsRecorder<RoomDirectoryEvents>()
|
||||
val state = aRoomDirectoryState(
|
||||
roomDescriptions = aRoomDescriptionList(),
|
||||
eventSink = eventsRecorder,
|
||||
)
|
||||
val clickedRoom = state.roomDescriptions.first()
|
||||
ensureCalledOnceWithParam(clickedRoom) { callback ->
|
||||
rule.setRoomDirectoryView(
|
||||
state = state,
|
||||
onResultClick = callback,
|
||||
)
|
||||
rule.onNodeWithText(clickedRoom.computedName).performClick()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `composing load more indicator emits expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<RoomDirectoryEvents>()
|
||||
val state = aRoomDirectoryState(
|
||||
displayLoadMoreIndicator = true,
|
||||
eventSink = eventsRecorder,
|
||||
)
|
||||
rule.setRoomDirectoryView(state = state)
|
||||
eventsRecorder.assertSingle(RoomDirectoryEvents.LoadMore)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomDirectoryView(
|
||||
state: RoomDirectoryState,
|
||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||
onResultClick: (RoomDescription) -> Unit = EnsureNeverCalledWithParam(),
|
||||
) {
|
||||
setContent {
|
||||
RoomDirectoryView(
|
||||
state = state,
|
||||
onResultClick = onResultClick,
|
||||
onBackClick = onBackClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user