forked from dsutanto/bChot-android
First Commit
This commit is contained in:
19
features/space/api/build.gradle.kts
Normal file
19
features/space/api/build.gradle.kts
Normal file
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.space.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.space.api
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
interface SpaceEntryPoint : FeatureEntryPoint {
|
||||
fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
inputs: Inputs,
|
||||
callback: Callback
|
||||
): Node
|
||||
|
||||
data class Inputs(
|
||||
val roomId: RoomId
|
||||
) : NodeInputs
|
||||
|
||||
interface Callback : Plugin {
|
||||
fun navigateToRoom(roomId: RoomId, viaParameters: List<String>)
|
||||
fun navigateToRoomMemberList()
|
||||
}
|
||||
}
|
||||
50
features/space/impl/build.gradle.kts
Normal file
50
features/space/impl/build.gradle.kts
Normal file
@@ -0,0 +1,50 @@
|
||||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
id("kotlin-parcelize")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.space.impl"
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
isIncludeAndroidResources = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setupDependencyInjection()
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.deeplink.api)
|
||||
implementation(projects.services.analytics.api)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(projects.libraries.featureflag.api)
|
||||
implementation(projects.features.invite.api)
|
||||
implementation(projects.libraries.previewutils)
|
||||
api(projects.features.space.api)
|
||||
|
||||
testCommonDependencies(libs, true)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.featureflag.test)
|
||||
testImplementation(projects.features.invite.test)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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.space.impl
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.features.space.api.SpaceEntryPoint
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class DefaultSpaceEntryPoint : SpaceEntryPoint {
|
||||
override fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
inputs: SpaceEntryPoint.Inputs,
|
||||
callback: SpaceEntryPoint.Callback,
|
||||
): Node {
|
||||
return parentNode.createNode<SpaceFlowNode>(
|
||||
buildContext = buildContext,
|
||||
plugins = listOf(inputs, callback),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
package io.element.android.features.space.impl
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.lifecycle.subscribe
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.pop
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.space.api.SpaceEntryPoint
|
||||
import io.element.android.features.space.impl.di.SpaceFlowGraph
|
||||
import io.element.android.features.space.impl.leave.LeaveSpaceNode
|
||||
import io.element.android.features.space.impl.root.SpaceNode
|
||||
import io.element.android.features.space.impl.settings.SpaceSettingsNode
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.DependencyInjectionGraphOwner
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceService
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
@AssistedInject
|
||||
class SpaceFlowNode(
|
||||
@Assisted val buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
room: JoinedRoom,
|
||||
spaceService: SpaceService,
|
||||
graphFactory: SpaceFlowGraph.Factory,
|
||||
) : BaseFlowNode<SpaceFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Root,
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
), DependencyInjectionGraphOwner {
|
||||
private val callback: SpaceEntryPoint.Callback = callback()
|
||||
private val spaceRoomList = spaceService.spaceRoomList(room.roomId)
|
||||
override val graph = graphFactory.create(spaceRoomList)
|
||||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
data object Root : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object Settings : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object Leave : NavTarget
|
||||
}
|
||||
|
||||
override fun onBuilt() {
|
||||
super.onBuilt()
|
||||
lifecycle.subscribe(
|
||||
onDestroy = {
|
||||
spaceRoomList.destroy()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
NavTarget.Leave -> {
|
||||
val callback = object : LeaveSpaceNode.Callback {
|
||||
override fun closeLeaveSpaceFlow() {
|
||||
backstack.pop()
|
||||
}
|
||||
|
||||
override fun navigateToRolesAndPermissions() {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
createNode<LeaveSpaceNode>(buildContext, listOf(callback))
|
||||
}
|
||||
NavTarget.Root -> {
|
||||
val callback = object : SpaceNode.Callback {
|
||||
override fun navigateToRoom(roomId: RoomId, viaParameters: List<String>) {
|
||||
callback.navigateToRoom(roomId, viaParameters)
|
||||
}
|
||||
|
||||
override fun navigateToSpaceSettings() {
|
||||
backstack.push(NavTarget.Settings)
|
||||
}
|
||||
|
||||
override fun navigateToRoomMemberList() {
|
||||
callback.navigateToRoomMemberList()
|
||||
}
|
||||
|
||||
override fun startLeaveSpaceFlow() {
|
||||
backstack.push(NavTarget.Leave)
|
||||
}
|
||||
}
|
||||
createNode<SpaceNode>(buildContext, listOf(callback))
|
||||
}
|
||||
NavTarget.Settings -> {
|
||||
val callback = object : SpaceSettingsNode.Callback {
|
||||
override fun closeSettings() {
|
||||
backstack.pop()
|
||||
}
|
||||
|
||||
override fun navigateToSpaceInfo() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
override fun navigateToSpaceMembers() {
|
||||
callback.navigateToRoomMemberList()
|
||||
}
|
||||
|
||||
override fun navigateToRolesAndPermissions() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
override fun navigateToSecurityAndPrivacy() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
override fun startLeaveSpaceFlow() {
|
||||
backstack.push(NavTarget.Leave)
|
||||
}
|
||||
}
|
||||
createNode<SpaceSettingsNode>(buildContext, listOf(callback))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) = BackstackView()
|
||||
}
|
||||
@@ -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.space.impl.di
|
||||
|
||||
import dev.zacsweers.metro.ContributesTo
|
||||
import dev.zacsweers.metro.GraphExtension
|
||||
import dev.zacsweers.metro.Provides
|
||||
import io.element.android.libraries.architecture.NodeFactoriesBindings
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
||||
|
||||
@GraphExtension(SpaceFlowScope::class)
|
||||
interface SpaceFlowGraph : NodeFactoriesBindings {
|
||||
@ContributesTo(RoomScope::class)
|
||||
@GraphExtension.Factory
|
||||
interface Factory {
|
||||
fun create(@Provides spaceRoomList: SpaceRoomList): SpaceFlowGraph
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* 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.space.impl.di
|
||||
|
||||
abstract class SpaceFlowScope private constructor()
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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.space.impl.leave
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
sealed interface LeaveSpaceEvents {
|
||||
data object Retry : LeaveSpaceEvents
|
||||
data object SelectAllRooms : LeaveSpaceEvents
|
||||
data object DeselectAllRooms : LeaveSpaceEvents
|
||||
data class ToggleRoomSelection(val roomId: RoomId) : LeaveSpaceEvents
|
||||
data object LeaveSpace : LeaveSpaceEvents
|
||||
data object CloseError : LeaveSpaceEvents
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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.space.impl.leave
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.lifecycle.subscribe
|
||||
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.space.impl.di.SpaceFlowScope
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
|
||||
@ContributesNode(SpaceFlowScope::class)
|
||||
@AssistedInject
|
||||
class LeaveSpaceNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
matrixClient: MatrixClient,
|
||||
room: JoinedRoom,
|
||||
presenterFactory: LeaveSpacePresenter.Factory,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
interface Callback : Plugin {
|
||||
fun closeLeaveSpaceFlow()
|
||||
fun navigateToRolesAndPermissions()
|
||||
}
|
||||
|
||||
private val leaveSpaceHandle = matrixClient.spaceService.getLeaveSpaceHandle(room.roomId)
|
||||
private val presenter: LeaveSpacePresenter = presenterFactory.create(leaveSpaceHandle)
|
||||
|
||||
private val callback: Callback = callback()
|
||||
|
||||
override fun onBuilt() {
|
||||
super.onBuilt()
|
||||
lifecycle.subscribe(
|
||||
onDestroy = {
|
||||
leaveSpaceHandle.close()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
LeaveSpaceView(
|
||||
state = state,
|
||||
onCancel = callback::closeLeaveSpaceFlow,
|
||||
onRolesAndPermissionsClick = callback::navigateToRolesAndPermissions,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* 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.space.impl.leave
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.map
|
||||
import io.element.android.libraries.architecture.runUpdatingState
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.spaces.LeaveSpaceHandle
|
||||
import io.element.android.libraries.matrix.api.spaces.LeaveSpaceRoom
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@AssistedInject
|
||||
class LeaveSpacePresenter(
|
||||
@Assisted private val leaveSpaceHandle: LeaveSpaceHandle,
|
||||
) : Presenter<LeaveSpaceState> {
|
||||
@AssistedFactory
|
||||
fun interface Factory {
|
||||
fun create(leaveSpaceHandle: LeaveSpaceHandle): LeaveSpacePresenter
|
||||
}
|
||||
|
||||
data class LeaveSpaceRooms(
|
||||
val current: LeaveSpaceRoom?,
|
||||
val others: List<LeaveSpaceRoom>,
|
||||
)
|
||||
|
||||
@Composable
|
||||
override fun present(): LeaveSpaceState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var retryCount by remember { mutableIntStateOf(0) }
|
||||
val leaveSpaceAction = remember {
|
||||
mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
|
||||
}
|
||||
var selectedRoomIds by remember {
|
||||
mutableStateOf<Collection<RoomId>>(setOf())
|
||||
}
|
||||
var leaveSpaceRooms by remember {
|
||||
mutableStateOf<AsyncData<LeaveSpaceRooms>>(AsyncData.Loading())
|
||||
}
|
||||
LaunchedEffect(retryCount) {
|
||||
val rooms = leaveSpaceHandle.rooms()
|
||||
val (currentRoom, otherRooms) = rooms.getOrNull()
|
||||
.orEmpty()
|
||||
.partition { it.spaceRoom.roomId == leaveSpaceHandle.id }
|
||||
// By default select all rooms that can be left
|
||||
val otherRoomsExcludingDm = otherRooms.filter { it.spaceRoom.isDirect != true }
|
||||
selectedRoomIds = otherRoomsExcludingDm
|
||||
.filter { it.isLastAdmin.not() }
|
||||
.map { it.spaceRoom.roomId }
|
||||
leaveSpaceRooms = rooms.fold(
|
||||
onSuccess = {
|
||||
AsyncData.Success(
|
||||
LeaveSpaceRooms(
|
||||
current = currentRoom.firstOrNull(),
|
||||
others = otherRoomsExcludingDm.toImmutableList(),
|
||||
)
|
||||
)
|
||||
},
|
||||
onFailure = { AsyncData.Failure(it) }
|
||||
)
|
||||
}
|
||||
var selectableSpaceRooms by remember {
|
||||
mutableStateOf<AsyncData<ImmutableList<SelectableSpaceRoom>>>(AsyncData.Loading())
|
||||
}
|
||||
LaunchedEffect(selectedRoomIds, leaveSpaceRooms) {
|
||||
selectableSpaceRooms = leaveSpaceRooms.map {
|
||||
it.others.map { room ->
|
||||
SelectableSpaceRoom(
|
||||
spaceRoom = room.spaceRoom,
|
||||
isLastAdmin = room.isLastAdmin,
|
||||
isSelected = selectedRoomIds.contains(room.spaceRoom.roomId),
|
||||
)
|
||||
}.toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
fun handleEvent(event: LeaveSpaceEvents) {
|
||||
when (event) {
|
||||
LeaveSpaceEvents.Retry -> {
|
||||
leaveSpaceRooms = AsyncData.Loading()
|
||||
retryCount += 1
|
||||
}
|
||||
LeaveSpaceEvents.DeselectAllRooms -> {
|
||||
selectedRoomIds = persistentSetOf()
|
||||
}
|
||||
LeaveSpaceEvents.SelectAllRooms -> {
|
||||
selectedRoomIds = selectableSpaceRooms.dataOrNull()
|
||||
.orEmpty()
|
||||
.filter { it.isLastAdmin.not() }
|
||||
.map { it.spaceRoom.roomId }
|
||||
}
|
||||
is LeaveSpaceEvents.ToggleRoomSelection -> {
|
||||
selectedRoomIds = if (selectedRoomIds.contains(event.roomId)) {
|
||||
selectedRoomIds - event.roomId
|
||||
} else {
|
||||
selectedRoomIds + event.roomId
|
||||
}
|
||||
}
|
||||
LeaveSpaceEvents.LeaveSpace -> coroutineScope.leaveSpace(
|
||||
leaveSpaceAction = leaveSpaceAction,
|
||||
selectedRoomIds = selectedRoomIds,
|
||||
)
|
||||
LeaveSpaceEvents.CloseError -> {
|
||||
leaveSpaceAction.value = AsyncAction.Uninitialized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return LeaveSpaceState(
|
||||
spaceName = leaveSpaceRooms.dataOrNull()?.current?.spaceRoom?.displayName,
|
||||
isLastAdmin = leaveSpaceRooms.dataOrNull()?.current?.isLastAdmin == true,
|
||||
selectableSpaceRooms = selectableSpaceRooms,
|
||||
leaveSpaceAction = leaveSpaceAction.value,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.leaveSpace(
|
||||
leaveSpaceAction: MutableState<AsyncAction<Unit>>,
|
||||
selectedRoomIds: Collection<RoomId>,
|
||||
) = launch {
|
||||
runUpdatingState(leaveSpaceAction) {
|
||||
leaveSpaceHandle.leave(selectedRoomIds.toList())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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.space.impl.leave
|
||||
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
data class LeaveSpaceState(
|
||||
val spaceName: String?,
|
||||
val isLastAdmin: Boolean,
|
||||
val selectableSpaceRooms: AsyncData<ImmutableList<SelectableSpaceRoom>>,
|
||||
val leaveSpaceAction: AsyncAction<Unit>,
|
||||
val eventSink: (LeaveSpaceEvents) -> Unit,
|
||||
) {
|
||||
private val rooms = selectableSpaceRooms.dataOrNull().orEmpty().toImmutableList()
|
||||
private val lastAdminRooms: ImmutableList<SelectableSpaceRoom>
|
||||
private val selectableRooms: ImmutableList<SelectableSpaceRoom>
|
||||
|
||||
init {
|
||||
val partition = rooms.partition { it.isLastAdmin }
|
||||
lastAdminRooms = partition.first.toImmutableList()
|
||||
selectableRooms = partition.second.toImmutableList()
|
||||
}
|
||||
|
||||
/**
|
||||
* True if we should show the quick action to select/deselect all rooms.
|
||||
*/
|
||||
val showQuickAction = isLastAdmin.not() && selectableRooms.isNotEmpty()
|
||||
|
||||
/**
|
||||
* True if we should show the leave button.
|
||||
*/
|
||||
val showLeaveButton = isLastAdmin.not() && selectableSpaceRooms is AsyncData.Success
|
||||
|
||||
/**
|
||||
* True if there all the selectable rooms are selected.
|
||||
*/
|
||||
val areAllSelected = selectableRooms.all { it.isSelected }
|
||||
|
||||
/**
|
||||
* True if there are rooms but the user is the last admin in all of them.
|
||||
*/
|
||||
val hasOnlyLastAdminRoom = lastAdminRooms.isNotEmpty() && selectableRooms.isEmpty()
|
||||
|
||||
/**
|
||||
* Number of selected rooms.
|
||||
*/
|
||||
val selectedRoomsCount = selectableRooms.count { it.isSelected }
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* 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.space.impl.leave
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
class LeaveSpaceStateProvider : PreviewParameterProvider<LeaveSpaceState> {
|
||||
override val values: Sequence<LeaveSpaceState>
|
||||
get() = sequenceOf(
|
||||
aLeaveSpaceState(),
|
||||
aLeaveSpaceState(
|
||||
spaceName = null,
|
||||
selectableSpaceRooms = AsyncData.Success(persistentListOf()),
|
||||
),
|
||||
aLeaveSpaceState(
|
||||
selectableSpaceRooms = AsyncData.Success(
|
||||
persistentListOf(
|
||||
aSelectableSpaceRoom(
|
||||
spaceRoom = aSpaceRoom(
|
||||
displayName = "A long space name that should be truncated",
|
||||
worldReadable = true,
|
||||
),
|
||||
isLastAdmin = true,
|
||||
),
|
||||
aSelectableSpaceRoom(
|
||||
spaceRoom = aSpaceRoom(
|
||||
joinRule = JoinRule.Private,
|
||||
),
|
||||
isSelected = false,
|
||||
),
|
||||
)
|
||||
)
|
||||
),
|
||||
aLeaveSpaceState(
|
||||
selectableSpaceRooms = AsyncData.Success(
|
||||
persistentListOf(
|
||||
aSelectableSpaceRoom(
|
||||
spaceRoom = aSpaceRoom(
|
||||
worldReadable = true,
|
||||
),
|
||||
isLastAdmin = true,
|
||||
),
|
||||
aSelectableSpaceRoom(
|
||||
spaceRoom = aSpaceRoom(
|
||||
joinRule = JoinRule.Private,
|
||||
),
|
||||
isSelected = true,
|
||||
),
|
||||
)
|
||||
)
|
||||
),
|
||||
aLeaveSpaceState(
|
||||
selectableSpaceRooms = AsyncData.Success(
|
||||
persistentListOf(
|
||||
aSelectableSpaceRoom(
|
||||
spaceRoom = aSpaceRoom(
|
||||
worldReadable = true,
|
||||
),
|
||||
isLastAdmin = true,
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
aLeaveSpaceState(
|
||||
selectableSpaceRooms = AsyncData.Success(
|
||||
persistentListOf(
|
||||
aSelectableSpaceRoom(
|
||||
spaceRoom = aSpaceRoom(
|
||||
worldReadable = true,
|
||||
),
|
||||
isLastAdmin = true,
|
||||
),
|
||||
aSelectableSpaceRoom(
|
||||
spaceRoom = aSpaceRoom(),
|
||||
isLastAdmin = true,
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
aLeaveSpaceState(
|
||||
selectableSpaceRooms = AsyncData.Success(
|
||||
List(10) { aSelectableSpaceRoom() }.toImmutableList()
|
||||
),
|
||||
leaveSpaceAction = AsyncAction.Loading,
|
||||
),
|
||||
aLeaveSpaceState(
|
||||
selectableSpaceRooms = AsyncData.Success(
|
||||
List(10) { aSelectableSpaceRoom() }.toImmutableList()
|
||||
),
|
||||
leaveSpaceAction = AsyncAction.Failure(Exception("An error")),
|
||||
),
|
||||
aLeaveSpaceState(
|
||||
selectableSpaceRooms = AsyncData.Failure(Exception("An error")),
|
||||
),
|
||||
aLeaveSpaceState(
|
||||
isLastAdmin = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun aLeaveSpaceState(
|
||||
spaceName: String? = "Space name",
|
||||
isLastAdmin: Boolean = false,
|
||||
selectableSpaceRooms: AsyncData<ImmutableList<SelectableSpaceRoom>> = AsyncData.Uninitialized,
|
||||
leaveSpaceAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
) = LeaveSpaceState(
|
||||
spaceName = spaceName,
|
||||
isLastAdmin = isLastAdmin,
|
||||
selectableSpaceRooms = selectableSpaceRooms,
|
||||
leaveSpaceAction = leaveSpaceAction,
|
||||
eventSink = { }
|
||||
)
|
||||
|
||||
fun aSelectableSpaceRoom(
|
||||
spaceRoom: SpaceRoom = aSpaceRoom(),
|
||||
isLastAdmin: Boolean = false,
|
||||
isSelected: Boolean = false,
|
||||
) = SelectableSpaceRoom(
|
||||
spaceRoom = spaceRoom,
|
||||
isLastAdmin = isLastAdmin,
|
||||
isSelected = isSelected,
|
||||
)
|
||||
@@ -0,0 +1,363 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
package io.element.android.features.space.impl.leave
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.selection.toggleable
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
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.space.impl.R
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||
import io.element.android.libraries.designsystem.components.BigIcon
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncFailure
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncLoading
|
||||
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.Button
|
||||
import io.element.android.libraries.designsystem.theme.components.Checkbox
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.IconSource
|
||||
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.TopAppBar
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
import io.element.android.libraries.ui.strings.CommonPlurals
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
/**
|
||||
* https://www.figma.com/design/kcnHxunG1LDWXsJhaNuiHz/ER-145--Spaces-on-Element-X?node-id=3947-68767&t=GTf1cLkAf6UCQDan-0
|
||||
*/
|
||||
@Composable
|
||||
fun LeaveSpaceView(
|
||||
state: LeaveSpaceState,
|
||||
onCancel: () -> Unit,
|
||||
onRolesAndPermissionsClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
LeaveSpaceHeader(
|
||||
state = state,
|
||||
onBackClick = onCancel,
|
||||
)
|
||||
},
|
||||
containerColor = ElementTheme.colors.bgCanvasDefault,
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.imePadding()
|
||||
.consumeWindowInsets(padding)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
) {
|
||||
if (state.isLastAdmin.not()) {
|
||||
when (state.selectableSpaceRooms) {
|
||||
is AsyncData.Success -> {
|
||||
// List rooms where the user is the only admin
|
||||
state.selectableSpaceRooms.data.forEach { selectableSpaceRoom ->
|
||||
item {
|
||||
SpaceItem(
|
||||
selectableSpaceRoom = selectableSpaceRoom,
|
||||
showCheckBox = state.hasOnlyLastAdminRoom.not(),
|
||||
onClick = {
|
||||
state.eventSink(LeaveSpaceEvents.ToggleRoomSelection(selectableSpaceRoom.spaceRoom.roomId))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is AsyncData.Failure -> item {
|
||||
AsyncFailure(
|
||||
throwable = state.selectableSpaceRooms.error,
|
||||
onRetry = {
|
||||
state.eventSink(LeaveSpaceEvents.Retry)
|
||||
},
|
||||
)
|
||||
}
|
||||
is AsyncData.Loading,
|
||||
AsyncData.Uninitialized -> item {
|
||||
AsyncLoading()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
LeaveSpaceButtons(
|
||||
showLeaveButton = state.showLeaveButton,
|
||||
selectedRoomsCount = state.selectedRoomsCount,
|
||||
onLeaveSpace = {
|
||||
state.eventSink(LeaveSpaceEvents.LeaveSpace)
|
||||
},
|
||||
onCancel = onCancel,
|
||||
// TODO enable when navigation is ready
|
||||
showRolesAndPermissionsButton = false, // state.isLastAdmin,
|
||||
onRolesAndPermissionsClick = onRolesAndPermissionsClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AsyncActionView(
|
||||
async = state.leaveSpaceAction,
|
||||
onSuccess = { /* Nothing to do, the screen will be dismissed automatically */ },
|
||||
errorMessage = { stringResource(CommonStrings.error_unknown) },
|
||||
onErrorDismiss = { state.eventSink(LeaveSpaceEvents.CloseError) },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LeaveSpaceHeader(
|
||||
state: LeaveSpaceState,
|
||||
onBackClick: () -> Unit,
|
||||
) {
|
||||
Column {
|
||||
TopAppBar(
|
||||
navigationIcon = {
|
||||
BackButton(onClick = onBackClick)
|
||||
},
|
||||
title = {},
|
||||
)
|
||||
IconTitleSubtitleMolecule(
|
||||
modifier = Modifier.padding(top = 0.dp, bottom = 8.dp, start = 24.dp, end = 24.dp),
|
||||
iconStyle = BigIcon.Style.AlertSolid,
|
||||
title = stringResource(
|
||||
if (state.isLastAdmin) R.string.screen_leave_space_title_last_admin else R.string.screen_leave_space_title,
|
||||
state.spaceName ?: stringResource(CommonStrings.common_space)
|
||||
),
|
||||
subTitle =
|
||||
if (state.isLastAdmin) {
|
||||
stringResource(R.string.screen_leave_space_subtitle_last_admin)
|
||||
} else if (state.selectableSpaceRooms is AsyncData.Success && state.selectableSpaceRooms.data.isNotEmpty()) {
|
||||
if (state.hasOnlyLastAdminRoom) {
|
||||
stringResource(R.string.screen_leave_space_subtitle_only_last_admin)
|
||||
} else {
|
||||
stringResource(R.string.screen_leave_space_subtitle)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
if (state.showQuickAction) {
|
||||
if (state.areAllSelected) {
|
||||
QuickActionButton(CommonStrings.action_deselect_all) {
|
||||
state.eventSink(LeaveSpaceEvents.DeselectAllRooms)
|
||||
}
|
||||
} else {
|
||||
QuickActionButton(resId = CommonStrings.action_select_all) {
|
||||
state.eventSink(LeaveSpaceEvents.SelectAllRooms)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.QuickActionButton(
|
||||
@StringRes resId: Int,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.align(Alignment.End)
|
||||
.padding(end = 8.dp)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(8.dp),
|
||||
text = stringResource(resId),
|
||||
color = ElementTheme.colors.textActionPrimary,
|
||||
style = ElementTheme.typography.fontBodyMdMedium,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LeaveSpaceButtons(
|
||||
showLeaveButton: Boolean,
|
||||
selectedRoomsCount: Int,
|
||||
onLeaveSpace: () -> Unit,
|
||||
showRolesAndPermissionsButton: Boolean,
|
||||
onRolesAndPermissionsClick: () -> Unit,
|
||||
onCancel: () -> Unit,
|
||||
) {
|
||||
ButtonColumnMolecule(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
if (showLeaveButton) {
|
||||
val text = if (selectedRoomsCount > 0) {
|
||||
pluralStringResource(R.plurals.screen_leave_space_submit, selectedRoomsCount, selectedRoomsCount)
|
||||
} else {
|
||||
stringResource(CommonStrings.action_leave_space)
|
||||
}
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = text,
|
||||
leadingIcon = IconSource.Vector(CompoundIcons.Leave()),
|
||||
onClick = onLeaveSpace,
|
||||
destructive = true,
|
||||
)
|
||||
}
|
||||
if (showRolesAndPermissionsButton) {
|
||||
Button(
|
||||
text = stringResource(CommonStrings.action_go_to_roles_and_permissions),
|
||||
onClick = onRolesAndPermissionsClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
leadingIcon = IconSource.Vector(CompoundIcons.Settings()),
|
||||
)
|
||||
}
|
||||
TextButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResource(CommonStrings.action_cancel),
|
||||
onClick = onCancel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SpaceItem(
|
||||
selectableSpaceRoom: SelectableSpaceRoom,
|
||||
showCheckBox: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val room = selectableSpaceRoom.spaceRoom
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 66.dp)
|
||||
.toggleable(
|
||||
value = selectableSpaceRoom.isSelected,
|
||||
role = Role.Checkbox,
|
||||
enabled = selectableSpaceRoom.isLastAdmin.not(),
|
||||
onValueChange = { onClick() }
|
||||
)
|
||||
.clickable(
|
||||
enabled = selectableSpaceRoom.isLastAdmin.not(),
|
||||
// TODO
|
||||
onClickLabel = null,
|
||||
role = Role.Checkbox,
|
||||
onClick = onClick,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Avatar(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
avatarData = room.getAvatarData(AvatarSize.LeaveSpaceRoom),
|
||||
avatarType = if (room.isSpace) AvatarType.Space() else AvatarType.Room(),
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp),
|
||||
text = room.displayName,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
style = ElementTheme.typography.fontBodyLgMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (room.joinRule == JoinRule.Private) {
|
||||
// Picto for private
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.padding(end = 4.dp),
|
||||
imageVector = CompoundIcons.LockSolid(),
|
||||
contentDescription = null,
|
||||
tint = ElementTheme.colors.iconTertiary,
|
||||
)
|
||||
} else if (room.worldReadable) {
|
||||
// Picto for world readable
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.padding(end = 4.dp),
|
||||
imageVector = CompoundIcons.Public(),
|
||||
contentDescription = null,
|
||||
tint = ElementTheme.colors.iconTertiary,
|
||||
)
|
||||
}
|
||||
// Number of members
|
||||
val membersCount = pluralStringResource(
|
||||
CommonPlurals.common_member_count,
|
||||
room.numJoinedMembers,
|
||||
room.numJoinedMembers
|
||||
)
|
||||
val subTitle = if (selectableSpaceRoom.isLastAdmin) {
|
||||
stringResource(R.string.screen_leave_space_last_admin_info, membersCount)
|
||||
} else {
|
||||
membersCount
|
||||
}
|
||||
Text(
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
text = subTitle,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (showCheckBox) {
|
||||
Checkbox(
|
||||
checked = selectableSpaceRoom.isSelected,
|
||||
onCheckedChange = null,
|
||||
enabled = selectableSpaceRoom.isLastAdmin.not(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun LeaveSpaceViewPreview(
|
||||
@PreviewParameter(LeaveSpaceStateProvider::class) state: LeaveSpaceState,
|
||||
) = ElementPreview {
|
||||
LeaveSpaceView(
|
||||
state = state,
|
||||
onCancel = {},
|
||||
onRolesAndPermissionsClick = {},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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.space.impl.leave
|
||||
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
|
||||
data class SelectableSpaceRoom(
|
||||
val spaceRoom: SpaceRoom,
|
||||
val isLastAdmin: Boolean,
|
||||
val isSelected: Boolean,
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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.space.impl.root
|
||||
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
|
||||
sealed interface SpaceEvents {
|
||||
data object LoadMore : SpaceEvents
|
||||
data class Join(val spaceRoom: SpaceRoom) : SpaceEvents
|
||||
data object ClearFailures : SpaceEvents
|
||||
data class AcceptInvite(val spaceRoom: SpaceRoom) : SpaceEvents
|
||||
data class DeclineInvite(val spaceRoom: SpaceRoom) : SpaceEvents
|
||||
|
||||
data class ShowTopicViewer(val topic: String) : SpaceEvents
|
||||
data object HideTopicViewer : SpaceEvents
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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.space.impl.root
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
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.invite.api.acceptdecline.AcceptDeclineInviteView
|
||||
import io.element.android.features.space.impl.di.SpaceFlowScope
|
||||
import io.element.android.libraries.androidutils.R
|
||||
import io.element.android.libraries.androidutils.system.startSharePlainTextIntent
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
@ContributesNode(SpaceFlowScope::class)
|
||||
@AssistedInject
|
||||
class SpaceNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: SpacePresenter,
|
||||
private val matrixClient: MatrixClient,
|
||||
private val spaceRoomList: SpaceRoomList,
|
||||
private val acceptDeclineInviteView: AcceptDeclineInviteView,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
interface Callback : Plugin {
|
||||
fun navigateToRoom(roomId: RoomId, viaParameters: List<String>)
|
||||
fun navigateToSpaceSettings()
|
||||
fun navigateToRoomMemberList()
|
||||
fun startLeaveSpaceFlow()
|
||||
}
|
||||
|
||||
private val callback: Callback = callback()
|
||||
|
||||
private fun onShareRoom(context: Context) = lifecycleScope.launch {
|
||||
matrixClient.getRoom(spaceRoomList.roomId)?.use { room ->
|
||||
room.getPermalink()
|
||||
.onSuccess { permalink ->
|
||||
context.startSharePlainTextIntent(
|
||||
activityResultLauncher = null,
|
||||
chooserTitle = context.getString(CommonStrings.common_share_space),
|
||||
text = permalink,
|
||||
noActivityFoundMessage = context.getString(R.string.error_no_compatible_app_found)
|
||||
)
|
||||
}
|
||||
.onFailure {
|
||||
Timber.e(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
val context = LocalContext.current
|
||||
SpaceView(
|
||||
state = state,
|
||||
onBackClick = ::navigateUp,
|
||||
onLeaveSpaceClick = {
|
||||
callback.startLeaveSpaceFlow()
|
||||
},
|
||||
onRoomClick = { spaceRoom ->
|
||||
callback.navigateToRoom(spaceRoom.roomId, spaceRoom.via)
|
||||
},
|
||||
onDetailsClick = {
|
||||
callback.navigateToSpaceSettings()
|
||||
},
|
||||
onShareSpace = {
|
||||
onShareRoom(context)
|
||||
},
|
||||
onViewMembersClick = {
|
||||
callback.navigateToRoomMemberList()
|
||||
},
|
||||
acceptDeclineInviteView = {
|
||||
acceptDeclineInviteView.Render(
|
||||
state = state.acceptDeclineInviteState,
|
||||
onAcceptInviteSuccess = { roomId ->
|
||||
callback.navigateToRoom(roomId, emptyList())
|
||||
},
|
||||
onDeclineInviteSuccess = { roomId ->
|
||||
// No action needed
|
||||
},
|
||||
modifier = Modifier
|
||||
)
|
||||
},
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* 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.space.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.setValue
|
||||
import dev.zacsweers.metro.Inject
|
||||
import im.vector.app.features.analytics.plan.JoinedRoom
|
||||
import io.element.android.features.invite.api.SeenInvitesStore
|
||||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
|
||||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
|
||||
import io.element.android.features.invite.api.toInviteData
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.coroutine.mapState
|
||||
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRoom
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
||||
import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
@Inject
|
||||
class SpacePresenter(
|
||||
private val spaceRoomList: SpaceRoomList,
|
||||
private val client: MatrixClient,
|
||||
private val seenInvitesStore: SeenInvitesStore,
|
||||
private val joinRoom: JoinRoom,
|
||||
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
||||
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
|
||||
) : Presenter<SpaceState> {
|
||||
private var children by mutableStateOf<ImmutableList<SpaceRoom>>(persistentListOf())
|
||||
|
||||
@Composable
|
||||
override fun present(): SpaceState {
|
||||
LaunchedEffect(Unit) {
|
||||
paginate()
|
||||
spaceRoomList.spaceRoomsFlow.collect { children = it.toImmutableList() }
|
||||
}
|
||||
|
||||
val hideInvitesAvatar by client.rememberHideInvitesAvatar()
|
||||
val seenSpaceInvites by remember {
|
||||
seenInvitesStore.seenRoomIds().map { it.toImmutableSet() }
|
||||
}.collectAsState(persistentSetOf())
|
||||
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
|
||||
val hasMoreToLoad by remember {
|
||||
spaceRoomList.paginationStatusFlow.mapState { status ->
|
||||
when (status) {
|
||||
is SpaceRoomList.PaginationStatus.Idle -> status.hasMoreToLoad
|
||||
SpaceRoomList.PaginationStatus.Loading -> true
|
||||
}
|
||||
}
|
||||
}.collectAsState()
|
||||
|
||||
val currentSpace by spaceRoomList.currentSpaceFlow.collectAsState()
|
||||
val (joinActions, setJoinActions) = remember { mutableStateOf(emptyMap<RoomId, AsyncAction<Unit>>()) }
|
||||
|
||||
var topicViewerState: TopicViewerState by remember { mutableStateOf(TopicViewerState.Hidden) }
|
||||
|
||||
LaunchedEffect(children) {
|
||||
// Remove joined children from the join actions
|
||||
val joinedChildren = children
|
||||
.filter { it.state == CurrentUserMembership.JOINED }
|
||||
.map { it.roomId }
|
||||
setJoinActions(joinActions - joinedChildren)
|
||||
}
|
||||
|
||||
val acceptDeclineInviteState = acceptDeclineInvitePresenter.present()
|
||||
|
||||
fun handleEvent(event: SpaceEvents) {
|
||||
when (event) {
|
||||
SpaceEvents.LoadMore -> localCoroutineScope.paginate()
|
||||
is SpaceEvents.Join -> {
|
||||
sessionCoroutineScope.joinRoom(event.spaceRoom, joinActions, setJoinActions)
|
||||
}
|
||||
SpaceEvents.ClearFailures -> {
|
||||
val failedActions = joinActions
|
||||
.filterValues { it is AsyncAction.Failure }
|
||||
.mapValues { AsyncAction.Uninitialized }
|
||||
setJoinActions(joinActions + failedActions)
|
||||
}
|
||||
is SpaceEvents.AcceptInvite -> {
|
||||
acceptDeclineInviteState.eventSink(
|
||||
AcceptDeclineInviteEvents.AcceptInvite(event.spaceRoom.toInviteData())
|
||||
)
|
||||
}
|
||||
is SpaceEvents.DeclineInvite -> {
|
||||
acceptDeclineInviteState.eventSink(
|
||||
AcceptDeclineInviteEvents.DeclineInvite(invite = event.spaceRoom.toInviteData(), shouldConfirm = true, blockUser = false)
|
||||
)
|
||||
}
|
||||
SpaceEvents.HideTopicViewer -> topicViewerState = TopicViewerState.Hidden
|
||||
is SpaceEvents.ShowTopicViewer -> topicViewerState = TopicViewerState.Shown(event.topic)
|
||||
}
|
||||
}
|
||||
return SpaceState(
|
||||
currentSpace = currentSpace.getOrNull(),
|
||||
children = children,
|
||||
seenSpaceInvites = seenSpaceInvites,
|
||||
hideInvitesAvatar = hideInvitesAvatar,
|
||||
hasMoreToLoad = hasMoreToLoad,
|
||||
joinActions = joinActions.toImmutableMap(),
|
||||
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||
topicViewerState = topicViewerState,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.joinRoom(
|
||||
spaceRoom: SpaceRoom,
|
||||
joinActions: Map<RoomId, AsyncAction<Unit>>,
|
||||
setJoinActions: (Map<RoomId, AsyncAction<Unit>>) -> Unit
|
||||
) = launch {
|
||||
setJoinActions(joinActions + mapOf(spaceRoom.roomId to AsyncAction.Loading))
|
||||
joinRoom.invoke(
|
||||
roomIdOrAlias = spaceRoom.roomId.toRoomIdOrAlias(),
|
||||
serverNames = spaceRoom.via,
|
||||
trigger = JoinedRoom.Trigger.SpaceHierarchy,
|
||||
).onFailure {
|
||||
setJoinActions(joinActions + mapOf(spaceRoom.roomId to AsyncAction.Failure(it)))
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.paginate() = launch {
|
||||
spaceRoomList.paginate()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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.space.impl.root
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
|
||||
data class SpaceState(
|
||||
val currentSpace: SpaceRoom?,
|
||||
val children: ImmutableList<SpaceRoom>,
|
||||
val seenSpaceInvites: ImmutableSet<RoomId>,
|
||||
val hideInvitesAvatar: Boolean,
|
||||
val hasMoreToLoad: Boolean,
|
||||
val joinActions: ImmutableMap<RoomId, AsyncAction<Unit>>,
|
||||
val acceptDeclineInviteState: AcceptDeclineInviteState,
|
||||
val topicViewerState: TopicViewerState,
|
||||
val eventSink: (SpaceEvents) -> Unit
|
||||
) {
|
||||
fun isJoining(spaceId: RoomId): Boolean = joinActions[spaceId] == AsyncAction.Loading
|
||||
val hasAnyFailure: Boolean = joinActions.values.any {
|
||||
it is AsyncAction.Failure
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
sealed interface TopicViewerState {
|
||||
data object Hidden : TopicViewerState
|
||||
data class Shown(val topic: String) : TopicViewerState
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 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.space.impl.root
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
|
||||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
|
||||
import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInviteState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
|
||||
open class SpaceStateProvider : PreviewParameterProvider<SpaceState> {
|
||||
override val values: Sequence<SpaceState>
|
||||
get() = sequenceOf(
|
||||
aSpaceState(),
|
||||
aSpaceState(parentSpace = aParentSpace(joinRule = JoinRule.Public)),
|
||||
aSpaceState(parentSpace = aParentSpace(joinRule = JoinRule.Restricted(persistentListOf()))),
|
||||
aSpaceState(children = aListOfSpaceRooms()),
|
||||
aSpaceState(
|
||||
parentSpace = aParentSpace(),
|
||||
children = aListOfSpaceRooms(),
|
||||
joiningRooms = setOf(RoomId("!spaceId0:example.com")),
|
||||
hasMoreToLoad = false
|
||||
),
|
||||
aSpaceState(
|
||||
topicViewerState = TopicViewerState.Shown(topic = "Space description goes here." + LoremIpsum(20).values.first()),
|
||||
),
|
||||
// Add other states here
|
||||
)
|
||||
}
|
||||
|
||||
fun aSpaceState(
|
||||
parentSpace: SpaceRoom? = aParentSpace(),
|
||||
children: List<SpaceRoom> = emptyList(),
|
||||
seenSpaceInvites: Set<RoomId> = emptySet(),
|
||||
joiningRooms: Set<RoomId> = emptySet(),
|
||||
joinActions: Map<RoomId, AsyncAction<Unit>> = joiningRooms.associateWith { AsyncAction.Loading },
|
||||
hideInvitesAvatar: Boolean = false,
|
||||
hasMoreToLoad: Boolean = true,
|
||||
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
|
||||
topicViewerState: TopicViewerState = TopicViewerState.Hidden,
|
||||
eventSink: (SpaceEvents) -> Unit = { },
|
||||
) = SpaceState(
|
||||
currentSpace = parentSpace,
|
||||
children = children.toImmutableList(),
|
||||
seenSpaceInvites = seenSpaceInvites.toImmutableSet(),
|
||||
hideInvitesAvatar = hideInvitesAvatar,
|
||||
hasMoreToLoad = hasMoreToLoad,
|
||||
joinActions = joinActions.toImmutableMap(),
|
||||
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||
topicViewerState = topicViewerState,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
private fun aParentSpace(
|
||||
joinRule: JoinRule? = null,
|
||||
): SpaceRoom {
|
||||
return aSpaceRoom(
|
||||
numJoinedMembers = 5,
|
||||
childrenCount = 10,
|
||||
worldReadable = true,
|
||||
joinRule = joinRule,
|
||||
roomId = RoomId("!spaceId0:example.com"),
|
||||
topic = "Space description goes here. " + LoremIpsum(20).values.first(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun aListOfSpaceRooms(): List<SpaceRoom> {
|
||||
return listOf(
|
||||
aSpaceRoom(
|
||||
roomId = RoomId("!spaceId0:example.com"),
|
||||
state = null,
|
||||
),
|
||||
aSpaceRoom(
|
||||
roomId = RoomId("!spaceId1:example.com"),
|
||||
state = CurrentUserMembership.JOINED,
|
||||
),
|
||||
aSpaceRoom(
|
||||
roomId = RoomId("!spaceId2:example.com"),
|
||||
state = CurrentUserMembership.INVITED,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,427 @@
|
||||
/*
|
||||
* 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.space.impl.root
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.heading
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
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.libraries.designsystem.atomic.molecules.InviteButtonsRowMolecule
|
||||
import io.element.android.libraries.designsystem.components.ClickableLinkText
|
||||
import io.element.android.libraries.designsystem.components.SimpleModalBottomSheet
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncIndicator
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncIndicatorHost
|
||||
import io.element.android.libraries.designsystem.components.async.rememberAsyncIndicatorState
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
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.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.DropdownMenu
|
||||
import io.element.android.libraries.designsystem.theme.components.DropdownMenuItem
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
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.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import io.element.android.libraries.matrix.ui.components.JoinButton
|
||||
import io.element.android.libraries.matrix.ui.components.SpaceHeaderView
|
||||
import io.element.android.libraries.matrix.ui.components.SpaceRoomItemView
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SpaceView(
|
||||
state: SpaceState,
|
||||
onBackClick: () -> Unit,
|
||||
onRoomClick: (spaceRoom: SpaceRoom) -> Unit,
|
||||
onShareSpace: () -> Unit,
|
||||
onLeaveSpaceClick: () -> Unit,
|
||||
onDetailsClick: () -> Unit,
|
||||
onViewMembersClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
acceptDeclineInviteView: @Composable () -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
SpaceViewTopBar(
|
||||
currentSpace = state.currentSpace,
|
||||
onBackClick = onBackClick,
|
||||
onLeaveSpaceClick = onLeaveSpaceClick,
|
||||
onShareSpace = onShareSpace,
|
||||
onDetailsClick = onDetailsClick,
|
||||
onViewMembersClick = onViewMembersClick,
|
||||
)
|
||||
},
|
||||
content = { padding ->
|
||||
Box(
|
||||
modifier = Modifier.padding(padding)
|
||||
) {
|
||||
SpaceViewContent(
|
||||
state = state,
|
||||
onRoomClick = onRoomClick,
|
||||
onTopicClick = { topic ->
|
||||
state.eventSink(SpaceEvents.ShowTopicViewer(topic))
|
||||
}
|
||||
)
|
||||
JoinRoomFailureEffect(
|
||||
hasAnyFailure = state.hasAnyFailure,
|
||||
eventSink = state.eventSink
|
||||
)
|
||||
acceptDeclineInviteView()
|
||||
}
|
||||
},
|
||||
)
|
||||
if (state.topicViewerState is TopicViewerState.Shown) {
|
||||
TopicViewerBottomSheet(
|
||||
topicViewerState = state.topicViewerState,
|
||||
onDismiss = {
|
||||
state.eventSink(SpaceEvents.HideTopicViewer)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun JoinRoomFailureEffect(
|
||||
hasAnyFailure: Boolean,
|
||||
eventSink: (SpaceEvents) -> Unit,
|
||||
) {
|
||||
val asyncIndicatorState = rememberAsyncIndicatorState()
|
||||
val updatedEventSink by rememberUpdatedState(eventSink)
|
||||
AsyncIndicatorHost(modifier = Modifier, asyncIndicatorState)
|
||||
LaunchedEffect(hasAnyFailure) {
|
||||
if (hasAnyFailure) {
|
||||
asyncIndicatorState.enqueue {
|
||||
AsyncIndicator.Failure(text = stringResource(CommonStrings.common_something_went_wrong))
|
||||
}
|
||||
delay(AsyncIndicator.DURATION_SHORT)
|
||||
updatedEventSink(SpaceEvents.ClearFailures)
|
||||
} else {
|
||||
asyncIndicatorState.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TopicViewerBottomSheet(
|
||||
topicViewerState: TopicViewerState.Shown,
|
||||
onDismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
SimpleModalBottomSheet(
|
||||
title = stringResource(CommonStrings.common_description),
|
||||
onDismiss = onDismiss,
|
||||
modifier = modifier
|
||||
) {
|
||||
ClickableLinkText(
|
||||
text = topicViewerState.topic,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SpaceViewContent(
|
||||
state: SpaceState,
|
||||
onRoomClick: (spaceRoom: SpaceRoom) -> Unit,
|
||||
onTopicClick: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyColumn(modifier.fillMaxSize()) {
|
||||
val currentSpace = state.currentSpace
|
||||
if (currentSpace != null) {
|
||||
item {
|
||||
SpaceHeaderView(
|
||||
avatarData = currentSpace.getAvatarData(AvatarSize.SpaceHeader),
|
||||
name = currentSpace.displayName,
|
||||
topic = currentSpace.topic,
|
||||
topicMaxLines = 2,
|
||||
visibility = currentSpace.visibility,
|
||||
heroes = currentSpace.heroes.toImmutableList(),
|
||||
numberOfMembers = currentSpace.numJoinedMembers,
|
||||
onTopicClick = onTopicClick
|
||||
)
|
||||
}
|
||||
item {
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
itemsIndexed(
|
||||
items = state.children,
|
||||
key = { _, spaceRoom -> spaceRoom.roomId }
|
||||
) { index, spaceRoom ->
|
||||
val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED
|
||||
val isCurrentlyJoining = state.isJoining(spaceRoom.roomId)
|
||||
SpaceRoomItemView(
|
||||
spaceRoom = spaceRoom,
|
||||
showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites,
|
||||
hideAvatars = isInvitation && state.hideInvitesAvatar,
|
||||
onClick = {
|
||||
onRoomClick(spaceRoom)
|
||||
},
|
||||
onLongClick = {
|
||||
// TODO
|
||||
},
|
||||
trailingAction = spaceRoom.trailingAction(isCurrentlyJoining = isCurrentlyJoining) {
|
||||
state.eventSink(SpaceEvents.Join(spaceRoom))
|
||||
},
|
||||
bottomAction = spaceRoom.inviteButtons(
|
||||
onAcceptClick = {
|
||||
state.eventSink(SpaceEvents.AcceptInvite(spaceRoom))
|
||||
},
|
||||
onDeclineClick = {
|
||||
state.eventSink(SpaceEvents.DeclineInvite(spaceRoom))
|
||||
}
|
||||
)
|
||||
)
|
||||
if (index != state.children.lastIndex) {
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
if (state.hasMoreToLoad) {
|
||||
item {
|
||||
LoadingMoreIndicator(eventSink = state.eventSink)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadingMoreIndicator(
|
||||
eventSink: (SpaceEvents) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
strokeWidth = 2.dp,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
val latestEventSink by rememberUpdatedState(eventSink)
|
||||
LaunchedEffect(Unit) {
|
||||
latestEventSink(SpaceEvents.LoadMore)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun SpaceViewTopBar(
|
||||
currentSpace: SpaceRoom?,
|
||||
onBackClick: () -> Unit,
|
||||
onLeaveSpaceClick: () -> Unit,
|
||||
onDetailsClick: () -> Unit,
|
||||
onShareSpace: () -> Unit,
|
||||
onViewMembersClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
TopAppBar(
|
||||
modifier = modifier,
|
||||
navigationIcon = {
|
||||
BackButton(onClick = onBackClick)
|
||||
},
|
||||
title = {
|
||||
if (currentSpace != null) {
|
||||
val roundedCornerShape = RoundedCornerShape(8.dp)
|
||||
SpaceAvatarAndNameRow(
|
||||
name = currentSpace.displayName,
|
||||
avatarData = currentSpace.getAvatarData(AvatarSize.TimelineRoom),
|
||||
modifier = Modifier
|
||||
.clip(roundedCornerShape)
|
||||
// TODO enable when screen ready for space
|
||||
.clickable(enabled = false, onClick = onDetailsClick)
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
IconButton(
|
||||
onClick = { showMenu = !showMenu }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.OverflowVertical(),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
showMenu = false
|
||||
onShareSpace()
|
||||
},
|
||||
text = { Text(stringResource(id = CommonStrings.action_share)) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.ShareAndroid(),
|
||||
tint = ElementTheme.colors.iconSecondary,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
showMenu = false
|
||||
onViewMembersClick()
|
||||
},
|
||||
text = { Text(stringResource(id = CommonStrings.screen_space_menu_action_members)) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.User(),
|
||||
tint = ElementTheme.colors.iconSecondary,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
showMenu = false
|
||||
onLeaveSpaceClick()
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(id = CommonStrings.action_leave_space),
|
||||
color = ElementTheme.colors.textCriticalPrimary,
|
||||
)
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Leave(),
|
||||
tint = ElementTheme.colors.iconCriticalPrimary,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SpaceAvatarAndNameRow(
|
||||
name: String?,
|
||||
avatarData: AvatarData,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Avatar(
|
||||
avatarData = avatarData,
|
||||
avatarType = AvatarType.Space(),
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
.semantics {
|
||||
heading()
|
||||
},
|
||||
text = name ?: stringResource(CommonStrings.common_no_space_name),
|
||||
style = ElementTheme.typography.fontBodyLgMedium,
|
||||
fontStyle = FontStyle.Italic.takeIf { name == null },
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun SpaceRoom.trailingAction(
|
||||
isCurrentlyJoining: Boolean,
|
||||
onClick: () -> Unit
|
||||
): @Composable (() -> Unit)? {
|
||||
return when (state) {
|
||||
null, CurrentUserMembership.LEFT -> {
|
||||
{
|
||||
JoinButton(
|
||||
showProgress = isCurrentlyJoining,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun SpaceRoom.inviteButtons(
|
||||
onAcceptClick: () -> Unit,
|
||||
onDeclineClick: () -> Unit,
|
||||
): @Composable (() -> Unit)? {
|
||||
return when (state) {
|
||||
CurrentUserMembership.INVITED -> {
|
||||
@Composable {
|
||||
InviteButtonsRowMolecule(
|
||||
onAcceptClick = onAcceptClick,
|
||||
onDeclineClick = onDeclineClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun SpaceViewPreview(
|
||||
@PreviewParameter(SpaceStateProvider::class) state: SpaceState
|
||||
) = ElementPreview {
|
||||
SpaceView(
|
||||
state = state,
|
||||
onRoomClick = {},
|
||||
onShareSpace = {},
|
||||
onLeaveSpaceClick = {},
|
||||
acceptDeclineInviteView = {},
|
||||
onDetailsClick = {},
|
||||
onViewMembersClick = {},
|
||||
onBackClick = {},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* 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.space.impl.settings
|
||||
|
||||
sealed interface SpaceSettingsEvents
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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.space.impl.settings
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
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.space.impl.di.SpaceFlowScope
|
||||
import io.element.android.libraries.architecture.appyx.launchMolecule
|
||||
import io.element.android.libraries.architecture.callback
|
||||
|
||||
@ContributesNode(SpaceFlowScope::class)
|
||||
@AssistedInject
|
||||
class SpaceSettingsNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: SpaceSettingsPresenter,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
interface Callback : Plugin {
|
||||
fun closeSettings()
|
||||
|
||||
fun navigateToSpaceInfo()
|
||||
fun navigateToSpaceMembers()
|
||||
fun navigateToRolesAndPermissions()
|
||||
fun navigateToSecurityAndPrivacy()
|
||||
fun startLeaveSpaceFlow()
|
||||
}
|
||||
|
||||
private val callback: Callback = callback()
|
||||
private val stateFlow = launchMolecule { presenter.present() }
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state by stateFlow.collectAsState()
|
||||
SpaceSettingsView(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onSpaceInfoClick = callback::navigateToSpaceInfo,
|
||||
onBackClick = callback::closeSettings,
|
||||
onMembersClick = callback::navigateToSpaceMembers,
|
||||
onRolesAndPermissionsClick = callback::navigateToRolesAndPermissions,
|
||||
onSecurityAndPrivacyClick = callback::navigateToSecurityAndPrivacy,
|
||||
onLeaveSpaceClick = callback::startLeaveSpaceFlow,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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.space.impl.settings
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.ui.room.isOwnUserAdmin
|
||||
|
||||
@Inject
|
||||
class SpaceSettingsPresenter(
|
||||
private val room: JoinedRoom,
|
||||
) : Presenter<SpaceSettingsState> {
|
||||
@Composable
|
||||
override fun present(): SpaceSettingsState {
|
||||
val roomInfo by room.roomInfoFlow.collectAsState()
|
||||
val isUserAdmin = room.isOwnUserAdmin()
|
||||
return SpaceSettingsState(
|
||||
roomId = room.roomId,
|
||||
name = roomInfo.name.orEmpty(),
|
||||
canonicalAlias = roomInfo.canonicalAlias,
|
||||
avatarUrl = roomInfo.avatarUrl,
|
||||
memberCount = roomInfo.activeMembersCount,
|
||||
showRolesAndPermissions = isUserAdmin,
|
||||
showSecurityAndPrivacy = isUserAdmin,
|
||||
eventSink = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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.space.impl.settings
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
data class SpaceSettingsState(
|
||||
val roomId: RoomId,
|
||||
val name: String,
|
||||
val canonicalAlias: RoomAlias?,
|
||||
val avatarUrl: String?,
|
||||
val memberCount: Long,
|
||||
val showRolesAndPermissions: Boolean,
|
||||
val showSecurityAndPrivacy: Boolean,
|
||||
val eventSink: (SpaceSettingsEvents) -> Unit
|
||||
)
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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.space.impl.settings
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
open class SpaceSettingsStateProvider : PreviewParameterProvider<SpaceSettingsState> {
|
||||
override val values: Sequence<SpaceSettingsState>
|
||||
get() = sequenceOf(
|
||||
aSpaceSettingsState(),
|
||||
aSpaceSettingsState(alias = null),
|
||||
aSpaceSettingsState(showSecurityAndPrivacy = true),
|
||||
aSpaceSettingsState(showRolesAndPermissions = true),
|
||||
)
|
||||
}
|
||||
|
||||
fun aSpaceSettingsState(
|
||||
roomId: RoomId = RoomId("!aRoomId:element.io"),
|
||||
name: String = "Space name",
|
||||
alias: RoomAlias? = RoomAlias("#spacename:element.io"),
|
||||
avatarUrl: String? = null,
|
||||
memberCount: Long = 100,
|
||||
showRolesAndPermissions: Boolean = false,
|
||||
showSecurityAndPrivacy: Boolean = false,
|
||||
eventSink: (SpaceSettingsEvents) -> Unit = {},
|
||||
) = SpaceSettingsState(
|
||||
roomId = roomId,
|
||||
name = name,
|
||||
canonicalAlias = alias,
|
||||
avatarUrl = avatarUrl,
|
||||
memberCount = memberCount,
|
||||
showRolesAndPermissions = showRolesAndPermissions,
|
||||
showSecurityAndPrivacy = showSecurityAndPrivacy,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* 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.space.impl.settings
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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.space.impl.R
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
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.AvatarType
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.IconSource
|
||||
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||
import io.element.android.libraries.designsystem.theme.components.ListItemStyle
|
||||
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.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
fun SpaceSettingsView(
|
||||
state: SpaceSettingsState,
|
||||
onBackClick: () -> Unit,
|
||||
onSpaceInfoClick: () -> Unit,
|
||||
onMembersClick: () -> Unit,
|
||||
onRolesAndPermissionsClick: () -> Unit,
|
||||
onSecurityAndPrivacyClick: () -> Unit,
|
||||
onLeaveSpaceClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
SpaceSettingsTopBar(onBackClick = onBackClick)
|
||||
},
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
SpaceInfoSection(
|
||||
roomId = state.roomId,
|
||||
name = state.name,
|
||||
avatarUrl = state.avatarUrl,
|
||||
canonicalAlias = state.canonicalAlias?.value,
|
||||
onSpaceInfoClick = onSpaceInfoClick,
|
||||
)
|
||||
Section(isVisible = state.showSecurityAndPrivacy, content = {
|
||||
SecurityAndPrivacyItem(
|
||||
onClick = onSecurityAndPrivacyClick
|
||||
)
|
||||
})
|
||||
Section(content = {
|
||||
MembersItem(state.memberCount, onClick = onMembersClick)
|
||||
if (state.showRolesAndPermissions) {
|
||||
RolesAndPermissionsItem(onClick = onRolesAndPermissionsClick)
|
||||
}
|
||||
})
|
||||
Section(content = {
|
||||
LeaveSpaceItem(
|
||||
onClick = onLeaveSpaceClick
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SpaceInfoSection(
|
||||
roomId: RoomId,
|
||||
name: String,
|
||||
avatarUrl: String?,
|
||||
canonicalAlias: String?,
|
||||
onSpaceInfoClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onSpaceInfoClick)
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Avatar(
|
||||
avatarData = AvatarData(roomId.value, name, avatarUrl, AvatarSize.SpaceListItem),
|
||||
avatarType = AvatarType.Space(),
|
||||
contentDescription = avatarUrl?.let { stringResource(CommonStrings.a11y_avatar) },
|
||||
)
|
||||
Spacer(Modifier.width(16.dp))
|
||||
Column {
|
||||
Text(
|
||||
text = name,
|
||||
style = ElementTheme.typography.fontHeadingMdRegular,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
)
|
||||
if (canonicalAlias != null) {
|
||||
Text(
|
||||
text = canonicalAlias,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Section(
|
||||
modifier: Modifier = Modifier,
|
||||
isVisible: Boolean = true,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
if (isVisible) {
|
||||
PreferenceCategory(content = content, modifier = modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun SpaceSettingsTopBar(
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
TopAppBar(
|
||||
titleStr = stringResource(CommonStrings.common_settings),
|
||||
navigationIcon = { BackButton(onClick = onBackClick) },
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SecurityAndPrivacyItem(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_space_settings_security_and_privacy)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Lock())),
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MembersItem(
|
||||
memberCount: Long,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(CommonStrings.common_people)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.User())),
|
||||
trailingContent = ListItemContent.Text(memberCount.toString()),
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RolesAndPermissionsItem(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_space_settings_roles_and_permissions)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Admin())),
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LeaveSpaceItem(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(stringResource(CommonStrings.action_leave_space))
|
||||
},
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Leave())),
|
||||
style = ListItemStyle.Destructive,
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun SpaceSettingsViewPreview(
|
||||
@PreviewParameter(SpaceSettingsStateProvider::class) state: SpaceSettingsState
|
||||
) = ElementPreview {
|
||||
SpaceSettingsView(
|
||||
state = state,
|
||||
onBackClick = {},
|
||||
onSpaceInfoClick = {},
|
||||
onMembersClick = {},
|
||||
onRolesAndPermissionsClick = {},
|
||||
onSecurityAndPrivacyClick = {},
|
||||
onLeaveSpaceClick = {},
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_space_settings_roles_and_permissions">"Ролі і дазволы"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_space_settings_leave_space">"Напускане на пространството"</string>
|
||||
<string name="screen_space_settings_roles_and_permissions">"Роли и разрешения"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Защита и поверителност"</string>
|
||||
</resources>
|
||||
17
features/space/impl/src/main/res/values-cs/translations.xml
Normal file
17
features/space/impl/src/main/res/values-cs/translations.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_leave_space_last_admin_info">"%1$s (Správce)"</string>
|
||||
<plurals name="screen_leave_space_submit">
|
||||
<item quantity="one">"Opustit %1$d místnost a prostor"</item>
|
||||
<item quantity="few">"Opustit %1$d místnosti a prostor"</item>
|
||||
<item quantity="other">"Opustit %1$d místností a prostor"</item>
|
||||
</plurals>
|
||||
<string name="screen_leave_space_subtitle">"Tím budete také odstraněni ze všech místností v tomto prostoru."</string>
|
||||
<string name="screen_leave_space_subtitle_last_admin">"Než budete moci odejít, musíte pro tento prostor přiřadit jiného správce."</string>
|
||||
<string name="screen_leave_space_subtitle_only_last_admin">"Z následujících místností nebudete odstraněni, protože jste jediným administrátorem:"</string>
|
||||
<string name="screen_leave_space_title">"Opustit %1$s?"</string>
|
||||
<string name="screen_leave_space_title_last_admin">"Jste jediným administrátorem pro %1$s"</string>
|
||||
<string name="screen_space_settings_leave_space">"Opustit prostor"</string>
|
||||
<string name="screen_space_settings_roles_and_permissions">"Role a oprávnění"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Zabezpečení a soukromí"</string>
|
||||
</resources>
|
||||
17
features/space/impl/src/main/res/values-cy/translations.xml
Normal file
17
features/space/impl/src/main/res/values-cy/translations.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_leave_space_last_admin_info">"%1$s (Gweinyddwr)"</string>
|
||||
<plurals name="screen_leave_space_submit">
|
||||
<item quantity="zero">"Gadael %1$d ystafelloedd a gofodau"</item>
|
||||
<item quantity="one">"Gadael %1$d ystafell a gofod"</item>
|
||||
<item quantity="two">"Gadael %1$d ystafell a gofod"</item>
|
||||
<item quantity="few">"Gadael %1$d ystafell a gofod"</item>
|
||||
<item quantity="many">"Gadael %1$d ystafell a gofod"</item>
|
||||
<item quantity="other">"Gadael %1$d ystafell a gofod"</item>
|
||||
</plurals>
|
||||
<string name="screen_leave_space_subtitle">"Dewiswch yr ystafelloedd yr hoffech chi eu gadael nad chi yw\'r unig weinyddwr ar eu cyfer:"</string>
|
||||
<string name="screen_leave_space_title">"Gadael %1$s ?"</string>
|
||||
<string name="screen_space_settings_leave_space">"Gadael y gofod"</string>
|
||||
<string name="screen_space_settings_roles_and_permissions">"Rolau a chaniatâd"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Diogelwch a phreifatrwydd"</string>
|
||||
</resources>
|
||||
16
features/space/impl/src/main/res/values-da/translations.xml
Normal file
16
features/space/impl/src/main/res/values-da/translations.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_leave_space_last_admin_info">"%1$s (Admin)"</string>
|
||||
<plurals name="screen_leave_space_submit">
|
||||
<item quantity="one">"Forlad %1$d rum og gruppe"</item>
|
||||
<item quantity="other">"Forlad %1$d rum og Grupper"</item>
|
||||
</plurals>
|
||||
<string name="screen_leave_space_subtitle">"Vælg de rum, du vil forlade, som du ikke er den eneste administrator for:"</string>
|
||||
<string name="screen_leave_space_subtitle_last_admin">"Du skal tildele en anden administrator til denne gruppe, før du kan forlade den."</string>
|
||||
<string name="screen_leave_space_subtitle_only_last_admin">"Du vil ikke blive fjernet fra følgende rum, fordi du er den eneste administrator:"</string>
|
||||
<string name="screen_leave_space_title">"Forlad %1$s?"</string>
|
||||
<string name="screen_leave_space_title_last_admin">"Du er den eneste administrator for %1$s"</string>
|
||||
<string name="screen_space_settings_leave_space">"Forlad gruppe"</string>
|
||||
<string name="screen_space_settings_roles_and_permissions">"Roller og tilladelser"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Sikkerhed og privatliv"</string>
|
||||
</resources>
|
||||
16
features/space/impl/src/main/res/values-de/translations.xml
Normal file
16
features/space/impl/src/main/res/values-de/translations.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_leave_space_last_admin_info">"%1$s (Admin)"</string>
|
||||
<plurals name="screen_leave_space_submit">
|
||||
<item quantity="one">"%1$d Chat und Space verlassen"</item>
|
||||
<item quantity="other">"%1$d Chats und Space verlassen"</item>
|
||||
</plurals>
|
||||
<string name="screen_leave_space_subtitle">"Dadurch wirst du auch aus allen Chats in diesem Space entfernt."</string>
|
||||
<string name="screen_leave_space_subtitle_last_admin">"Du musst einen anderen Admin für diesen Space zuweisen, bevor du ihn verlassen kannst."</string>
|
||||
<string name="screen_leave_space_subtitle_only_last_admin">"Du wirst aus den folgenden Chats nicht entfernt, weil du der einzige Admin bist:"</string>
|
||||
<string name="screen_leave_space_title">"%1$s verlassen?"</string>
|
||||
<string name="screen_leave_space_title_last_admin">"Du bist der einzige Administrator für %1$s"</string>
|
||||
<string name="screen_space_settings_leave_space">"Space verlassen"</string>
|
||||
<string name="screen_space_settings_roles_and_permissions">"Rollen und Berechtigungen"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Sicherheit & Datenschutz"</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_space_settings_roles_and_permissions">"Ρόλοι και δικαιώματα"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Ασφάλεια & απόρρητο"</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_space_settings_roles_and_permissions">"Roles y permisos"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Seguridad y privacidad"</string>
|
||||
</resources>
|
||||
16
features/space/impl/src/main/res/values-et/translations.xml
Normal file
16
features/space/impl/src/main/res/values-et/translations.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_leave_space_last_admin_info">"%1$s (Peakasutaja)"</string>
|
||||
<plurals name="screen_leave_space_submit">
|
||||
<item quantity="one">"Lahku %1$d-st jututoast ja kogukonnast"</item>
|
||||
<item quantity="other">"Lahku %1$d-st jututoast ja kogukonnast"</item>
|
||||
</plurals>
|
||||
<string name="screen_leave_space_subtitle">"Sellega eemaldad end ka kõikidest antud kogukonna jututubadest."</string>
|
||||
<string name="screen_leave_space_subtitle_last_admin">"Enne lahkumist pead sa selle kogukonna jaoks lisama vähemalt ühe täiendava peakasutaja."</string>
|
||||
<string name="screen_leave_space_subtitle_only_last_admin">"Sind ei saa järgnevatest jututubadest eemaldada, kuna oled seal/neis ainus peakasutaja:"</string>
|
||||
<string name="screen_leave_space_title">"Kas lahkud %1$s kogukonnast?"</string>
|
||||
<string name="screen_leave_space_title_last_admin">"Sa oled siin ainus peakasutaja: %1$s"</string>
|
||||
<string name="screen_space_settings_leave_space">"Lahku kogukonnast"</string>
|
||||
<string name="screen_space_settings_roles_and_permissions">"Rollid ja õigused"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Turvalisus ja privaatsus"</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_space_settings_roles_and_permissions">"Rolak eta baimenak"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Segurtasuna eta pribatutasuna"</string>
|
||||
</resources>
|
||||
11
features/space/impl/src/main/res/values-fa/translations.xml
Normal file
11
features/space/impl/src/main/res/values-fa/translations.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_leave_space_last_admin_info">"%1$s (مدیر)"</string>
|
||||
<string name="screen_leave_space_subtitle_last_admin">"پیش از ترک باید مدیری دیگر به این فضا تخصیص دهید."</string>
|
||||
<string name="screen_leave_space_subtitle_only_last_admin">"از اتاق(های) زیر برداشته نخواهید شد؛ چرا که تنها مدیر هستید:"</string>
|
||||
<string name="screen_leave_space_title">"ترک %1$s؟"</string>
|
||||
<string name="screen_leave_space_title_last_admin">"تنها مدیر %1$s هستید"</string>
|
||||
<string name="screen_space_settings_leave_space">"ترک فضا"</string>
|
||||
<string name="screen_space_settings_roles_and_permissions">"نقشها و اجازهها"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"امنیت و محرمانگی"</string>
|
||||
</resources>
|
||||
16
features/space/impl/src/main/res/values-fi/translations.xml
Normal file
16
features/space/impl/src/main/res/values-fi/translations.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_leave_space_last_admin_info">"%1$s (Ylläpitäjä)"</string>
|
||||
<plurals name="screen_leave_space_submit">
|
||||
<item quantity="one">"Poistu %1$d huoneesta ja tilasta"</item>
|
||||
<item quantity="other">"Poistu %1$d huoneesta ja tilasta"</item>
|
||||
</plurals>
|
||||
<string name="screen_leave_space_subtitle">"Tämä poistaa sinut myös kaikista tämän tilan huoneista."</string>
|
||||
<string name="screen_leave_space_subtitle_last_admin">"Sinun on valittava tälle tilalle toinen ylläpitäjä ennen kuin voit poistua."</string>
|
||||
<string name="screen_leave_space_subtitle_only_last_admin">"Sinua ei poisteta seuraavista huoneista, koska olet ainoa ylläpitäjä:"</string>
|
||||
<string name="screen_leave_space_title">"Haluatko poistua tilasta %1$s?"</string>
|
||||
<string name="screen_leave_space_title_last_admin">"Olet ainoa ylläpitäjä tilassa %1$s"</string>
|
||||
<string name="screen_space_settings_leave_space">"Poistu tilasta"</string>
|
||||
<string name="screen_space_settings_roles_and_permissions">"Roolit ja oikeudet"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Turvallisuus ja yksityisyys"</string>
|
||||
</resources>
|
||||
16
features/space/impl/src/main/res/values-fr/translations.xml
Normal file
16
features/space/impl/src/main/res/values-fr/translations.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_leave_space_last_admin_info">"%1$s (Admin)"</string>
|
||||
<plurals name="screen_leave_space_submit">
|
||||
<item quantity="one">"Quitter %1$d salon et l’espace"</item>
|
||||
<item quantity="other">"Quitter %1$d salons et l’espace"</item>
|
||||
</plurals>
|
||||
<string name="screen_leave_space_subtitle">"Sélectionnez les salons que vous souhaitez quitter et dont vous n’êtes pas le seul administrateur:"</string>
|
||||
<string name="screen_leave_space_subtitle_last_admin">"Vous devez désigner un autre administrateur pour cet espace avant de pouvoir partir."</string>
|
||||
<string name="screen_leave_space_subtitle_only_last_admin">"Vous ne quitterez pas le ou les salons suivants car vous y êtes le seul administrateur:"</string>
|
||||
<string name="screen_leave_space_title">"Quitter %1$s?"</string>
|
||||
<string name="screen_leave_space_title_last_admin">"Vous êtes le seul administrateur de %1$s"</string>
|
||||
<string name="screen_space_settings_leave_space">"Quitter l’espace"</string>
|
||||
<string name="screen_space_settings_roles_and_permissions">"Rôles & autorisations"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Sécurité & confidentialité"</string>
|
||||
</resources>
|
||||
16
features/space/impl/src/main/res/values-hu/translations.xml
Normal file
16
features/space/impl/src/main/res/values-hu/translations.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_leave_space_last_admin_info">"%1$s (Adminisztrátor)"</string>
|
||||
<plurals name="screen_leave_space_submit">
|
||||
<item quantity="one">"%1$d szoba és tér elhagyása"</item>
|
||||
<item quantity="other">"%1$d szoba és tér elhagyása"</item>
|
||||
</plurals>
|
||||
<string name="screen_leave_space_subtitle">"Ez a tér összes szobájából is eltávolítja."</string>
|
||||
<string name="screen_leave_space_subtitle_last_admin">"Mielőtt elhagyhatná ezt a teret, ki kell jelölnie egy másik adminisztrátort."</string>
|
||||
<string name="screen_leave_space_subtitle_only_last_admin">"Nem lesz eltávolítva a következő szobá(k)ból, mert ön az egyetlen adminisztrátor:"</string>
|
||||
<string name="screen_leave_space_title">"Kilép innen: %1$s?"</string>
|
||||
<string name="screen_leave_space_title_last_admin">"Ön az egyetlen adminisztrátor itt: %1$s"</string>
|
||||
<string name="screen_space_settings_leave_space">"Tér elhagyása"</string>
|
||||
<string name="screen_space_settings_roles_and_permissions">"Szerepkörök és jogosultságok"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Biztonság és adatvédelem"</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_space_settings_roles_and_permissions">"Peran dan perizinan"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Keamanan & privasi"</string>
|
||||
</resources>
|
||||
16
features/space/impl/src/main/res/values-it/translations.xml
Normal file
16
features/space/impl/src/main/res/values-it/translations.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_leave_space_last_admin_info">"%1$s (Amministratore)"</string>
|
||||
<plurals name="screen_leave_space_submit">
|
||||
<item quantity="one">"Lascia %1$d stanza e spazio"</item>
|
||||
<item quantity="other">"Lascia %1$d stanze e spazi"</item>
|
||||
</plurals>
|
||||
<string name="screen_leave_space_subtitle">"Seleziona le stanze che desideri abbandonare e di cui non sei l\'unico amministratore:"</string>
|
||||
<string name="screen_leave_space_subtitle_last_admin">"Prima di poter uscire, devi assegnare un altro amministratore a questo spazio."</string>
|
||||
<string name="screen_leave_space_subtitle_only_last_admin">"Non verrai rimosso dalle seguenti stanze perché sei l\'unico amministratore:"</string>
|
||||
<string name="screen_leave_space_title">"Uscire da %1$s?"</string>
|
||||
<string name="screen_leave_space_title_last_admin">"Sei l\'unico amministratore di %1$s"</string>
|
||||
<string name="screen_space_settings_leave_space">"Esci dallo spazio"</string>
|
||||
<string name="screen_space_settings_roles_and_permissions">"Ruoli e autorizzazioni"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Sicurezza e privacy"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_space_settings_roles_and_permissions">"როლები და ნებართვები"</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_space_settings_roles_and_permissions">"역할 및 권한"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"보안 및 개인정보 보호"</string>
|
||||
</resources>
|
||||
16
features/space/impl/src/main/res/values-nb/translations.xml
Normal file
16
features/space/impl/src/main/res/values-nb/translations.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_leave_space_last_admin_info">"%1$s (Admin)"</string>
|
||||
<plurals name="screen_leave_space_submit">
|
||||
<item quantity="one">"Forlat %1$d rom og område"</item>
|
||||
<item quantity="other">"Forlat %1$d rom og område"</item>
|
||||
</plurals>
|
||||
<string name="screen_leave_space_subtitle">"Velg rommene du vil forlate, som du ikke er den eneste administratoren for:"</string>
|
||||
<string name="screen_leave_space_subtitle_last_admin">"Du må tildele en annen administrator for dette området før du kan forlate det."</string>
|
||||
<string name="screen_leave_space_subtitle_only_last_admin">"Du vil ikke bli fjernet fra følgende rom fordi du er den eneste administratoren:"</string>
|
||||
<string name="screen_leave_space_title">"Forlat %1$s?"</string>
|
||||
<string name="screen_leave_space_title_last_admin">"Du er den eneste administratoren for %1$s"</string>
|
||||
<string name="screen_space_settings_leave_space">"Forlat område"</string>
|
||||
<string name="screen_space_settings_roles_and_permissions">"Roller og tillatelser"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Sikkerhet og personvern"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_space_settings_roles_and_permissions">"Rollen en rechten"</string>
|
||||
</resources>
|
||||
17
features/space/impl/src/main/res/values-pl/translations.xml
Normal file
17
features/space/impl/src/main/res/values-pl/translations.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_leave_space_last_admin_info">"%1$s (Administrator)"</string>
|
||||
<plurals name="screen_leave_space_submit">
|
||||
<item quantity="one">"Opuść %1$d pokój i przestrzeń"</item>
|
||||
<item quantity="few">"Opuść %1$d pokoje i przestrzeń"</item>
|
||||
<item quantity="many">"Opuść %1$d pokojów i przestrzeń"</item>
|
||||
</plurals>
|
||||
<string name="screen_leave_space_subtitle">"Wybierz pokoje, które chcesz opuścić, a których nie jesteś jedynym administratorem:"</string>
|
||||
<string name="screen_leave_space_subtitle_last_admin">"Aby opuścić tę przestrzeń, musisz przypisać do niej innego administratora."</string>
|
||||
<string name="screen_leave_space_subtitle_only_last_admin">"Nie zostaniesz usunięty z następujących pokoi, ponieważ jesteś ich jedynym administratorem:"</string>
|
||||
<string name="screen_leave_space_title">"Opuścić %1$s?"</string>
|
||||
<string name="screen_leave_space_title_last_admin">"Jesteś jedynym administratorem %1$s"</string>
|
||||
<string name="screen_space_settings_leave_space">"Opuść przestrzeń"</string>
|
||||
<string name="screen_space_settings_roles_and_permissions">"Role i uprawnienia"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Bezpieczeństwo i prywatność"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_leave_space_last_admin_info">"%1$s (Administrador)"</string>
|
||||
<plurals name="screen_leave_space_submit">
|
||||
<item quantity="one">"Sair de %1$d sala e espaço"</item>
|
||||
<item quantity="other">"Sair de %1$d salas e do espaço"</item>
|
||||
</plurals>
|
||||
<string name="screen_leave_space_subtitle">"Selecione as salas que gostaria de sair nas quais você não é o único administrador:"</string>
|
||||
<string name="screen_leave_space_subtitle_last_admin">"Você precisa atribuir outro administrador para este espaço antes de sair."</string>
|
||||
<string name="screen_leave_space_subtitle_only_last_admin">"Você não será removido das seguintes salas porque você é o único administrador:"</string>
|
||||
<string name="screen_leave_space_title">"Sair de %1$s?"</string>
|
||||
<string name="screen_leave_space_title_last_admin">"Você é o único administrador de %1$s"</string>
|
||||
<string name="screen_space_settings_leave_space">"Sair do espaço"</string>
|
||||
<string name="screen_space_settings_roles_and_permissions">"Cargos e permissões"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Segurança e privacidade"</string>
|
||||
</resources>
|
||||
13
features/space/impl/src/main/res/values-pt/translations.xml
Normal file
13
features/space/impl/src/main/res/values-pt/translations.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_leave_space_last_admin_info">"%1$s (admin)"</string>
|
||||
<plurals name="screen_leave_space_submit">
|
||||
<item quantity="one">"Sair do espaço e de %1$d sala"</item>
|
||||
<item quantity="other">"Sair do espaço e de %1$d salas"</item>
|
||||
</plurals>
|
||||
<string name="screen_leave_space_subtitle">"Também irás sair de todas as salas deste espaço."</string>
|
||||
<string name="screen_leave_space_title">"Sair de %1$s?"</string>
|
||||
<string name="screen_space_settings_leave_space">"Sair do espaço"</string>
|
||||
<string name="screen_space_settings_roles_and_permissions">"Cargos e permissões"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Segurança e privacidade"</string>
|
||||
</resources>
|
||||
17
features/space/impl/src/main/res/values-ro/translations.xml
Normal file
17
features/space/impl/src/main/res/values-ro/translations.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_leave_space_last_admin_info">"%1$s (Admin)"</string>
|
||||
<plurals name="screen_leave_space_submit">
|
||||
<item quantity="one">"Părăsiți %1$d cameră și spațiul"</item>
|
||||
<item quantity="few">"Părăsiți %1$d camere și spațiul"</item>
|
||||
<item quantity="other">"Părăsiți %1$d camere și spațiul"</item>
|
||||
</plurals>
|
||||
<string name="screen_leave_space_subtitle">"Selectați camerele pe care doriți să le părăsiți și în care nu sunteți singurul administrator:"</string>
|
||||
<string name="screen_leave_space_subtitle_last_admin">"Trebuie să desemnați un alt administrator pentru acest spațiu înainte de a-l părăsi."</string>
|
||||
<string name="screen_leave_space_subtitle_only_last_admin">"Nu veți părăsi următoarele camere deoarece sunteți singurul administrator:"</string>
|
||||
<string name="screen_leave_space_title">"Părăsiți %1$s?"</string>
|
||||
<string name="screen_leave_space_title_last_admin">"Sunteți singurul administrator pentru %1$s"</string>
|
||||
<string name="screen_space_settings_leave_space">"Părăsiți spațiul"</string>
|
||||
<string name="screen_space_settings_roles_and_permissions">"Roluri și permisiuni"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Securitate & confidențialitate"</string>
|
||||
</resources>
|
||||
17
features/space/impl/src/main/res/values-ru/translations.xml
Normal file
17
features/space/impl/src/main/res/values-ru/translations.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_leave_space_last_admin_info">"%1$s (Администратор)"</string>
|
||||
<plurals name="screen_leave_space_submit">
|
||||
<item quantity="one">"Покинуть %1$d комнату и пространство"</item>
|
||||
<item quantity="few">"Покинуть %1$d комнат и пространство"</item>
|
||||
<item quantity="many">"Покинуть %1$d комнат и пространство"</item>
|
||||
</plurals>
|
||||
<string name="screen_leave_space_subtitle">"Выберите комнаты, которые вы хотите покинуть и в которых вы не являетесь единственным администратором:"</string>
|
||||
<string name="screen_leave_space_subtitle_last_admin">"Прежде чем покинуть это пространство, вам необходимо назначить другого администратора."</string>
|
||||
<string name="screen_leave_space_subtitle_only_last_admin">"Вы не будете удалены из следующих комнат, поскольку вы являетесь единственным администратором:"</string>
|
||||
<string name="screen_leave_space_title">"Выйти из %1$s?"</string>
|
||||
<string name="screen_leave_space_title_last_admin">"Вы единственный администратор для %1$s"</string>
|
||||
<string name="screen_space_settings_leave_space">"Покинуть пространство"</string>
|
||||
<string name="screen_space_settings_roles_and_permissions">"Роли и разрешения"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Безопасность и конфиденциальность"</string>
|
||||
</resources>
|
||||
17
features/space/impl/src/main/res/values-sk/translations.xml
Normal file
17
features/space/impl/src/main/res/values-sk/translations.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_leave_space_last_admin_info">"%1$s (Správca)"</string>
|
||||
<plurals name="screen_leave_space_submit">
|
||||
<item quantity="one">"Opustiť %1$d miestnosť a priestor"</item>
|
||||
<item quantity="few">"Opustiť %1$d miestnosti a priestory"</item>
|
||||
<item quantity="other">"Opustiť %1$d miestností a priestorov"</item>
|
||||
</plurals>
|
||||
<string name="screen_leave_space_subtitle">"Vyberte miestnosti, ktoré chcete opustiť a pre ktoré nie ste jediným správcom:"</string>
|
||||
<string name="screen_leave_space_subtitle_last_admin">"Pred odchodom musíte pre tento priestor určiť iného správcu."</string>
|
||||
<string name="screen_leave_space_subtitle_only_last_admin">"Z nasledujúcich miestností nebudete odstránený/á, pretože ste jediným správcom:"</string>
|
||||
<string name="screen_leave_space_title">"Opustiť %1$s?"</string>
|
||||
<string name="screen_leave_space_title_last_admin">"Ste jediným administrátorom pre %1$s"</string>
|
||||
<string name="screen_space_settings_leave_space">"Opustiť priestor"</string>
|
||||
<string name="screen_space_settings_roles_and_permissions">"Roly a povolenia"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Bezpečnosť a súkromie"</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_space_settings_roles_and_permissions">"Roller och behörigheter"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Säkerhet och sekretess"</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_space_settings_roles_and_permissions">"Roller ve izinler"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Güvenlik ve gizlilik"</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_space_settings_roles_and_permissions">"Ролі та дозволи"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Безпека й приватність"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_space_settings_roles_and_permissions">"کردارہا اور اجازتیں"</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_space_settings_roles_and_permissions">"Rollar va ruxsatlar"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Xavfsizlik va maxfiylik"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_leave_space_last_admin_info">"%1$s(管理員)"</string>
|
||||
<plurals name="screen_leave_space_submit">
|
||||
<item quantity="other">"離開 %1$d 個聊天室與空間"</item>
|
||||
</plurals>
|
||||
<string name="screen_leave_space_subtitle">"這也會將您從此空間中的所有聊天室移除。"</string>
|
||||
<string name="screen_leave_space_subtitle_last_admin">"您必須為此空間另外指定一位管理員後才能離開。"</string>
|
||||
<string name="screen_leave_space_subtitle_only_last_admin">"您不會被從以下聊天室移除,因為您是唯一的管理員:"</string>
|
||||
<string name="screen_leave_space_title">"離開 %1$s?"</string>
|
||||
<string name="screen_leave_space_title_last_admin">"您是 %1$s 唯一的管理員"</string>
|
||||
<string name="screen_space_settings_leave_space">"離開空間"</string>
|
||||
<string name="screen_space_settings_roles_and_permissions">"角色與權限"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"安全與隱私"</string>
|
||||
</resources>
|
||||
15
features/space/impl/src/main/res/values-zh/translations.xml
Normal file
15
features/space/impl/src/main/res/values-zh/translations.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_leave_space_last_admin_info">"%1$s (管理员)"</string>
|
||||
<plurals name="screen_leave_space_submit">
|
||||
<item quantity="other">"离开 %1$d 个房间和空间"</item>
|
||||
</plurals>
|
||||
<string name="screen_leave_space_subtitle">"选择您想要离开且您不是其唯一管理员的房间:"</string>
|
||||
<string name="screen_leave_space_subtitle_last_admin">"您需要为该空间指定另一位管理员才能离开。"</string>
|
||||
<string name="screen_leave_space_subtitle_only_last_admin">"您不会从以下房间中被移除,因为您是唯一的管理员:"</string>
|
||||
<string name="screen_leave_space_title">"离开%1$s?"</string>
|
||||
<string name="screen_leave_space_title_last_admin">"您是 %1$s 的唯一管理员"</string>
|
||||
<string name="screen_space_settings_leave_space">"离开空间"</string>
|
||||
<string name="screen_space_settings_roles_and_permissions">"角色与权限"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"安全与隐私"</string>
|
||||
</resources>
|
||||
16
features/space/impl/src/main/res/values/localazy.xml
Normal file
16
features/space/impl/src/main/res/values/localazy.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_leave_space_last_admin_info">"%1$s (Admin)"</string>
|
||||
<plurals name="screen_leave_space_submit">
|
||||
<item quantity="one">"Leave %1$d room and space"</item>
|
||||
<item quantity="other">"Leave %1$d rooms and space"</item>
|
||||
</plurals>
|
||||
<string name="screen_leave_space_subtitle">"Select the rooms you’d like to leave which you\'re not the only administrator for:"</string>
|
||||
<string name="screen_leave_space_subtitle_last_admin">"You need to assign another admin for this space before you can leave."</string>
|
||||
<string name="screen_leave_space_subtitle_only_last_admin">"You will not be removed from the following room(s) because you\'re the only administrator:"</string>
|
||||
<string name="screen_leave_space_title">"Leave %1$s?"</string>
|
||||
<string name="screen_leave_space_title_last_admin">"You are the only admin for %1$s"</string>
|
||||
<string name="screen_space_settings_leave_space">"Leave space"</string>
|
||||
<string name="screen_space_settings_roles_and_permissions">"Roles & permissions"</string>
|
||||
<string name="screen_space_settings_security_and_privacy">"Security & privacy"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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.space.impl
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.space.api.SpaceEntryPoint
|
||||
import io.element.android.features.space.impl.di.FakeSpaceFlowGraph
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||
import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList
|
||||
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.node.TestParentNode
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultSpaceEntryPointTest {
|
||||
@get:Rule
|
||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
@get:Rule
|
||||
val mainDispatcherRule = MainDispatcherRule()
|
||||
|
||||
@Test
|
||||
fun `test node builder`() {
|
||||
val entryPoint = DefaultSpaceEntryPoint()
|
||||
val nodeInputs = SpaceEntryPoint.Inputs(A_ROOM_ID)
|
||||
val parentNode = TestParentNode.create { buildContext, plugins ->
|
||||
SpaceFlowNode(
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
spaceService = FakeSpaceService(
|
||||
spaceRoomListResult = { _: RoomId -> FakeSpaceRoomList(A_ROOM_ID) }
|
||||
),
|
||||
room = FakeJoinedRoom(),
|
||||
graphFactory = FakeSpaceFlowGraph.Factory
|
||||
)
|
||||
}
|
||||
val callback = object : SpaceEntryPoint.Callback {
|
||||
override fun navigateToRoom(roomId: RoomId, viaParameters: List<String>) = lambdaError()
|
||||
override fun navigateToRoomMemberList() = lambdaError()
|
||||
}
|
||||
val result = entryPoint.createNode(
|
||||
parentNode = parentNode,
|
||||
buildContext = BuildContext.root(null),
|
||||
inputs = nodeInputs,
|
||||
callback = callback,
|
||||
)
|
||||
assertThat(result).isInstanceOf(SpaceFlowNode::class.java)
|
||||
assertThat(result.plugins).contains(nodeInputs)
|
||||
assertThat(result.plugins).contains(callback)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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.space.impl.di
|
||||
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import io.element.android.libraries.architecture.AssistedNodeFactory
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
class FakeSpaceFlowGraph : SpaceFlowGraph {
|
||||
object Factory : SpaceFlowGraph.Factory {
|
||||
override fun create(spaceRoomList: SpaceRoomList): SpaceFlowGraph {
|
||||
return FakeSpaceFlowGraph()
|
||||
}
|
||||
}
|
||||
|
||||
override fun nodeFactories(): Map<KClass<out Node>, AssistedNodeFactory<*>> {
|
||||
return emptyMap()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
/*
|
||||
* 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.space.impl.leave
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.spaces.LeaveSpaceHandle
|
||||
import io.element.android.libraries.matrix.api.spaces.LeaveSpaceRoom
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_3
|
||||
import io.element.android.libraries.matrix.test.A_SPACE_ID
|
||||
import io.element.android.libraries.matrix.test.A_SPACE_NAME
|
||||
import io.element.android.libraries.matrix.test.spaces.FakeLeaveSpaceHandle
|
||||
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import io.element.android.tests.testutils.test
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class LeaveSpacePresenterTest {
|
||||
private val aSpace = aSpaceRoom(
|
||||
roomId = A_SPACE_ID,
|
||||
displayName = A_SPACE_NAME,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = createLeaveSpacePresenter(
|
||||
leaveSpaceHandle = FakeLeaveSpaceHandle(
|
||||
roomsResult = { Result.success(emptyList()) },
|
||||
),
|
||||
)
|
||||
presenter.test {
|
||||
val state = awaitItem()
|
||||
assertThat(state.spaceName).isNull()
|
||||
assertThat(state.isLastAdmin).isFalse()
|
||||
assertThat(state.selectableSpaceRooms.isLoading()).isTrue()
|
||||
assertThat(state.leaveSpaceAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - fail to load rooms`() = runTest {
|
||||
val presenter = createLeaveSpacePresenter(
|
||||
leaveSpaceHandle = FakeLeaveSpaceHandle(
|
||||
roomsResult = { Result.failure(AN_EXCEPTION) },
|
||||
)
|
||||
)
|
||||
presenter.test {
|
||||
val state = awaitItem()
|
||||
assertThat(state.selectableSpaceRooms.isLoading()).isTrue()
|
||||
assertThat(state.leaveSpaceAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
skipItems(2)
|
||||
val stateError = awaitItem()
|
||||
assertThat(stateError.selectableSpaceRooms.isFailure()).isTrue()
|
||||
// Retry
|
||||
stateError.eventSink(LeaveSpaceEvents.Retry)
|
||||
skipItems(1)
|
||||
val stateLoadingAgain = awaitItem()
|
||||
assertThat(stateLoadingAgain.selectableSpaceRooms.isLoading()).isTrue()
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - current space name and is last admin`() = runTest {
|
||||
val presenter = createLeaveSpacePresenter(
|
||||
leaveSpaceHandle = FakeLeaveSpaceHandle(
|
||||
roomsResult = { Result.success(listOf(aLeaveSpaceRoom(spaceRoom = aSpace, isLastAdmin = true))) },
|
||||
)
|
||||
)
|
||||
presenter.test {
|
||||
val state = awaitItem()
|
||||
assertThat(state.spaceName).isNull()
|
||||
skipItems(2)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.spaceName).isEqualTo(A_SPACE_NAME)
|
||||
assertThat(finalState.isLastAdmin).isTrue()
|
||||
// The current state is not in the sub room list
|
||||
assertThat(finalState.selectableSpaceRooms.dataOrNull()!!).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - direct rooms are filtered out`() = runTest {
|
||||
val leaveResult = lambdaRecorder<List<RoomId>, Result<Unit>> { Result.success(Unit) }
|
||||
val presenter = createLeaveSpacePresenter(
|
||||
leaveSpaceHandle = FakeLeaveSpaceHandle(
|
||||
roomsResult = {
|
||||
Result.success(
|
||||
listOf(
|
||||
aLeaveSpaceRoom(spaceRoom = aSpace),
|
||||
aLeaveSpaceRoom(
|
||||
spaceRoom = aSpaceRoom(roomId = A_ROOM_ID, isDirect = false)
|
||||
),
|
||||
aLeaveSpaceRoom(
|
||||
spaceRoom = aSpaceRoom(roomId = A_ROOM_ID_2, isDirect = true)
|
||||
),
|
||||
aLeaveSpaceRoom(
|
||||
spaceRoom = aSpaceRoom(roomId = A_ROOM_ID_3, isDirect = null)
|
||||
),
|
||||
)
|
||||
)
|
||||
},
|
||||
leaveResult = leaveResult,
|
||||
)
|
||||
)
|
||||
presenter.test {
|
||||
val state = awaitItem()
|
||||
assertThat(state.spaceName).isNull()
|
||||
skipItems(2)
|
||||
val finalState = awaitItem()
|
||||
// The current state is not in the sub room list
|
||||
assertThat(finalState.selectableSpaceRooms.dataOrNull()!!.map { it.spaceRoom.roomId }).containsExactly(A_ROOM_ID, A_ROOM_ID_3)
|
||||
assertThat(finalState.selectedRoomsCount).isEqualTo(2)
|
||||
// Leaving the space will not include the DM
|
||||
finalState.eventSink(LeaveSpaceEvents.LeaveSpace)
|
||||
val stateLeaving = awaitItem()
|
||||
assertThat(stateLeaving.leaveSpaceAction).isEqualTo(AsyncAction.Loading)
|
||||
val stateLeft = awaitItem()
|
||||
assertThat(stateLeft.leaveSpaceAction.isSuccess()).isTrue()
|
||||
leaveResult.assertions().isCalledOnce().with(
|
||||
value(listOf(A_ROOM_ID, A_ROOM_ID_3))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - leave space and sub rooms`() = runTest {
|
||||
val leaveResult = lambdaRecorder<List<RoomId>, Result<Unit>> { Result.success(Unit) }
|
||||
val presenter = createLeaveSpacePresenter(
|
||||
leaveSpaceHandle = FakeLeaveSpaceHandle(
|
||||
roomsResult = {
|
||||
Result.success(
|
||||
listOf(
|
||||
LeaveSpaceRoom(aSpaceRoom(roomId = A_ROOM_ID), isLastAdmin = false),
|
||||
LeaveSpaceRoom(aSpaceRoom(roomId = A_ROOM_ID_2), isLastAdmin = true),
|
||||
)
|
||||
)
|
||||
},
|
||||
leaveResult = leaveResult,
|
||||
)
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(3)
|
||||
val state = awaitItem()
|
||||
assertThat(state.spaceName).isNull()
|
||||
assertThat(state.isLastAdmin).isFalse()
|
||||
val data = state.selectableSpaceRooms.dataOrNull()!!
|
||||
assertThat(data.size).isEqualTo(2)
|
||||
// Only one room is selectable as the user is the last admin in the other one
|
||||
val room1 = data[0]
|
||||
assertThat(room1.spaceRoom.roomId).isEqualTo(A_ROOM_ID)
|
||||
assertThat(room1.isSelected).isTrue()
|
||||
assertThat(room1.isLastAdmin).isFalse()
|
||||
val room2 = data[1]
|
||||
assertThat(room2.spaceRoom.roomId).isEqualTo(A_ROOM_ID_2)
|
||||
assertThat(room2.isSelected).isFalse()
|
||||
assertThat(room2.isLastAdmin).isTrue()
|
||||
// Deselect all
|
||||
state.eventSink(LeaveSpaceEvents.DeselectAllRooms)
|
||||
skipItems(1)
|
||||
val stateAllDeselected = awaitItem()
|
||||
val dataAllDeselected = stateAllDeselected.selectableSpaceRooms.dataOrNull()!!
|
||||
assertThat(dataAllDeselected.any { it.isSelected }).isFalse()
|
||||
// Select all
|
||||
stateAllDeselected.eventSink(LeaveSpaceEvents.SelectAllRooms)
|
||||
skipItems(1)
|
||||
val stateAllSelected = awaitItem()
|
||||
val dataAllSelected = stateAllSelected.selectableSpaceRooms.dataOrNull()!!
|
||||
// The last admin room should not be selected
|
||||
assertThat(dataAllSelected.count { it.isSelected }).isEqualTo(1)
|
||||
// Toggle selection of the first room
|
||||
stateAllSelected.eventSink(LeaveSpaceEvents.ToggleRoomSelection(A_ROOM_ID))
|
||||
skipItems(1)
|
||||
val stateOneDeselected = awaitItem()
|
||||
val dataOneDeselected = stateOneDeselected.selectableSpaceRooms.dataOrNull()!!
|
||||
assertThat(dataOneDeselected[0].isSelected).isFalse()
|
||||
// Toggle selection of the first room
|
||||
stateOneDeselected.eventSink(LeaveSpaceEvents.ToggleRoomSelection(A_ROOM_ID))
|
||||
skipItems(1)
|
||||
val stateOneSelected = awaitItem()
|
||||
val dataOneSelected = stateOneSelected.selectableSpaceRooms.dataOrNull()!!
|
||||
assertThat(dataOneSelected[0].isSelected).isTrue()
|
||||
// Leave space
|
||||
stateOneSelected.eventSink(LeaveSpaceEvents.LeaveSpace)
|
||||
val stateLeaving = awaitItem()
|
||||
assertThat(stateLeaving.leaveSpaceAction).isEqualTo(AsyncAction.Loading)
|
||||
val stateLeft = awaitItem()
|
||||
assertThat(stateLeft.leaveSpaceAction.isSuccess()).isTrue()
|
||||
leaveResult.assertions().isCalledOnce().with(
|
||||
value(listOf(A_ROOM_ID))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - leave space error and close`() = runTest {
|
||||
val leaveResult = lambdaRecorder<List<RoomId>, Result<Unit>> {
|
||||
Result.failure(AN_EXCEPTION)
|
||||
}
|
||||
val presenter = createLeaveSpacePresenter(
|
||||
leaveSpaceHandle = FakeLeaveSpaceHandle(
|
||||
roomsResult = { Result.success(emptyList()) },
|
||||
leaveResult = leaveResult,
|
||||
)
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(3)
|
||||
val state = awaitItem()
|
||||
state.eventSink(LeaveSpaceEvents.LeaveSpace)
|
||||
val stateLeaving = awaitItem()
|
||||
assertThat(stateLeaving.leaveSpaceAction).isEqualTo(AsyncAction.Loading)
|
||||
val stateError = awaitItem()
|
||||
assertThat(stateError.leaveSpaceAction.isFailure()).isTrue()
|
||||
// Close error
|
||||
stateError.eventSink(LeaveSpaceEvents.CloseError)
|
||||
val stateErrorClosed = awaitItem()
|
||||
assertThat(stateErrorClosed.leaveSpaceAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createLeaveSpacePresenter(
|
||||
leaveSpaceHandle: LeaveSpaceHandle = FakeLeaveSpaceHandle(),
|
||||
): LeaveSpacePresenter {
|
||||
return LeaveSpacePresenter(
|
||||
leaveSpaceHandle = leaveSpaceHandle,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun aLeaveSpaceRoom(
|
||||
spaceRoom: SpaceRoom = aSpaceRoom(
|
||||
roomId = A_SPACE_ID,
|
||||
displayName = A_SPACE_NAME,
|
||||
),
|
||||
isLastAdmin: Boolean = false,
|
||||
) = LeaveSpaceRoom(
|
||||
spaceRoom = spaceRoom,
|
||||
isLastAdmin = isLastAdmin,
|
||||
)
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* 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.space.impl.leave
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import org.junit.Test
|
||||
|
||||
class LeaveSpaceStateTest {
|
||||
@Test
|
||||
fun `test loading`() {
|
||||
val sut = aLeaveSpaceState(
|
||||
selectableSpaceRooms = AsyncData.Loading()
|
||||
)
|
||||
assertThat(sut.showQuickAction).isFalse()
|
||||
assertThat(sut.showLeaveButton).isFalse()
|
||||
assertThat(sut.areAllSelected).isTrue()
|
||||
assertThat(sut.hasOnlyLastAdminRoom).isFalse()
|
||||
assertThat(sut.selectedRoomsCount).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test no rooms`() {
|
||||
val sut = aLeaveSpaceState(
|
||||
selectableSpaceRooms = AsyncData.Success(
|
||||
persistentListOf()
|
||||
)
|
||||
)
|
||||
assertThat(sut.showQuickAction).isFalse()
|
||||
assertThat(sut.showLeaveButton).isTrue()
|
||||
assertThat(sut.areAllSelected).isTrue()
|
||||
assertThat(sut.hasOnlyLastAdminRoom).isFalse()
|
||||
assertThat(sut.selectedRoomsCount).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test last admin`() {
|
||||
val sut = aLeaveSpaceState(
|
||||
isLastAdmin = true,
|
||||
selectableSpaceRooms = AsyncData.Success(
|
||||
persistentListOf(
|
||||
aSelectableSpaceRoom(isLastAdmin = false, isSelected = false),
|
||||
)
|
||||
)
|
||||
)
|
||||
assertThat(sut.showQuickAction).isFalse()
|
||||
assertThat(sut.showLeaveButton).isFalse()
|
||||
assertThat(sut.areAllSelected).isFalse()
|
||||
assertThat(sut.hasOnlyLastAdminRoom).isFalse()
|
||||
assertThat(sut.selectedRoomsCount).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test no last admin, 1 selected, 1 not selected`() {
|
||||
val sut = aLeaveSpaceState(
|
||||
selectableSpaceRooms = AsyncData.Success(
|
||||
listOf(
|
||||
aSelectableSpaceRoom(isLastAdmin = false, isSelected = true),
|
||||
aSelectableSpaceRoom(isLastAdmin = false, isSelected = false),
|
||||
).toImmutableList()
|
||||
)
|
||||
)
|
||||
assertThat(sut.showQuickAction).isTrue()
|
||||
assertThat(sut.showLeaveButton).isTrue()
|
||||
assertThat(sut.areAllSelected).isFalse()
|
||||
assertThat(sut.hasOnlyLastAdminRoom).isFalse()
|
||||
assertThat(sut.selectedRoomsCount).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test no last admin, 2 selected`() {
|
||||
val sut = aLeaveSpaceState(
|
||||
selectableSpaceRooms = AsyncData.Success(
|
||||
listOf(
|
||||
aSelectableSpaceRoom(isLastAdmin = false, isSelected = true),
|
||||
aSelectableSpaceRoom(isLastAdmin = false, isSelected = true),
|
||||
).toImmutableList()
|
||||
)
|
||||
)
|
||||
assertThat(sut.showQuickAction).isTrue()
|
||||
assertThat(sut.showLeaveButton).isTrue()
|
||||
assertThat(sut.areAllSelected).isTrue()
|
||||
assertThat(sut.hasOnlyLastAdminRoom).isFalse()
|
||||
assertThat(sut.selectedRoomsCount).isEqualTo(2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test 1 last admin, 2 selected`() {
|
||||
val sut = aLeaveSpaceState(
|
||||
selectableSpaceRooms = AsyncData.Success(
|
||||
persistentListOf(
|
||||
aSelectableSpaceRoom(isLastAdmin = true, isSelected = false),
|
||||
aSelectableSpaceRoom(isLastAdmin = false, isSelected = true),
|
||||
aSelectableSpaceRoom(isLastAdmin = false, isSelected = true),
|
||||
)
|
||||
)
|
||||
)
|
||||
assertThat(sut.showQuickAction).isTrue()
|
||||
assertThat(sut.showLeaveButton).isTrue()
|
||||
assertThat(sut.areAllSelected).isTrue()
|
||||
assertThat(sut.hasOnlyLastAdminRoom).isFalse()
|
||||
assertThat(sut.selectedRoomsCount).isEqualTo(2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test only last admin`() {
|
||||
val sut = aLeaveSpaceState(
|
||||
selectableSpaceRooms = AsyncData.Success(
|
||||
listOf(
|
||||
aSelectableSpaceRoom(isLastAdmin = true, isSelected = false),
|
||||
aSelectableSpaceRoom(isLastAdmin = true, isSelected = false),
|
||||
).toImmutableList()
|
||||
)
|
||||
)
|
||||
assertThat(sut.showQuickAction).isFalse()
|
||||
assertThat(sut.showLeaveButton).isTrue()
|
||||
assertThat(sut.areAllSelected).isTrue()
|
||||
assertThat(sut.hasOnlyLastAdminRoom).isTrue()
|
||||
assertThat(sut.selectedRoomsCount).isEqualTo(0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.features.space.impl.root
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.invite.api.SeenInvitesStore
|
||||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
|
||||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
|
||||
import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInviteState
|
||||
import io.element.android.features.invite.api.toInviteData
|
||||
import io.element.android.features.invite.test.InMemorySeenInvitesStore
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRoom
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
|
||||
import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList
|
||||
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||
import io.element.android.tests.testutils.EventsRecorder
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import io.element.android.tests.testutils.test
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import im.vector.app.features.analytics.plan.JoinedRoom as AnalyticsJoinedRoom
|
||||
|
||||
class SpacePresenterTest {
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val paginateResult = lambdaRecorder<Result<Unit>> {
|
||||
Result.success(Unit)
|
||||
}
|
||||
val spaceRoomList = FakeSpaceRoomList(paginateResult = paginateResult)
|
||||
val presenter = createSpacePresenter(spaceRoomList = spaceRoomList)
|
||||
presenter.test {
|
||||
val state = awaitItem()
|
||||
assertThat(state.currentSpace).isNull()
|
||||
assertThat(state.children).isEmpty()
|
||||
assertThat(state.seenSpaceInvites).isEmpty()
|
||||
assertThat(state.hideInvitesAvatar).isFalse()
|
||||
assertThat(state.hasMoreToLoad).isTrue()
|
||||
assertThat(state.joinActions).isEmpty()
|
||||
assertThat(state.acceptDeclineInviteState).isEqualTo(anAcceptDeclineInviteState())
|
||||
assertThat(state.topicViewerState).isEqualTo(TopicViewerState.Hidden)
|
||||
advanceUntilIdle()
|
||||
paginateResult.assertions().isCalledOnce()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - load more`() = runTest {
|
||||
val paginateResult = lambdaRecorder<Result<Unit>> {
|
||||
Result.success(Unit)
|
||||
}
|
||||
val spaceRoomList = FakeSpaceRoomList(paginateResult = paginateResult)
|
||||
val presenter = createSpacePresenter(spaceRoomList = spaceRoomList)
|
||||
presenter.test {
|
||||
val state = awaitItem()
|
||||
advanceUntilIdle()
|
||||
paginateResult.assertions().isCalledOnce()
|
||||
state.eventSink(SpaceEvents.LoadMore)
|
||||
advanceUntilIdle()
|
||||
paginateResult.assertions().isCalledExactly(2)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - has more to load value`() = runTest {
|
||||
val paginateResult = lambdaRecorder<Result<Unit>> {
|
||||
Result.success(Unit)
|
||||
}
|
||||
val spaceRoomList = FakeSpaceRoomList(paginateResult = paginateResult)
|
||||
val presenter = createSpacePresenter(spaceRoomList = spaceRoomList)
|
||||
presenter.test {
|
||||
val state = awaitItem()
|
||||
advanceUntilIdle()
|
||||
assertThat(state.hasMoreToLoad).isTrue()
|
||||
spaceRoomList.emitPaginationStatus(
|
||||
SpaceRoomList.PaginationStatus.Idle(hasMoreToLoad = false)
|
||||
)
|
||||
assertThat(awaitItem().hasMoreToLoad).isFalse()
|
||||
spaceRoomList.emitPaginationStatus(
|
||||
SpaceRoomList.PaginationStatus.Idle(hasMoreToLoad = true)
|
||||
)
|
||||
assertThat(awaitItem().hasMoreToLoad).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - current space value`() = runTest {
|
||||
val paginateResult = lambdaRecorder<Result<Unit>> {
|
||||
Result.success(Unit)
|
||||
}
|
||||
val spaceRoomList = FakeSpaceRoomList(paginateResult = paginateResult)
|
||||
val presenter = createSpacePresenter(spaceRoomList = spaceRoomList)
|
||||
presenter.test {
|
||||
val state = awaitItem()
|
||||
advanceUntilIdle()
|
||||
assertThat(state.currentSpace).isNull()
|
||||
val aSpace = aSpaceRoom()
|
||||
spaceRoomList.emitCurrentSpace(aSpace)
|
||||
assertThat(awaitItem().currentSpace).isEqualTo(aSpace)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - children value`() = runTest {
|
||||
val paginateResult = lambdaRecorder<Result<Unit>> {
|
||||
Result.success(Unit)
|
||||
}
|
||||
val spaceRoomList = FakeSpaceRoomList(paginateResult = paginateResult)
|
||||
val presenter = createSpacePresenter(spaceRoomList = spaceRoomList)
|
||||
presenter.test {
|
||||
val state = awaitItem()
|
||||
advanceUntilIdle()
|
||||
assertThat(state.children).isEmpty()
|
||||
val aSpace = aSpaceRoom()
|
||||
spaceRoomList.emitSpaceRooms(listOf(aSpace))
|
||||
assertThat(awaitItem().children).containsExactly(aSpace)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - join a room success`() = runTest {
|
||||
val joinRoom = lambdaRecorder<RoomIdOrAlias, List<String>, AnalyticsJoinedRoom.Trigger, Result<Unit>> { _, _, _ ->
|
||||
Result.success(Unit)
|
||||
}
|
||||
val serverNames = listOf("via1", "via2")
|
||||
val aNotJoinedRoom = aSpaceRoom(
|
||||
roomId = A_ROOM_ID_2,
|
||||
via = serverNames,
|
||||
state = null,
|
||||
)
|
||||
val fakeSpaceRoomList = FakeSpaceRoomList(
|
||||
initialSpaceRoomsValue = listOf(
|
||||
aSpaceRoom(
|
||||
roomId = A_ROOM_ID,
|
||||
state = CurrentUserMembership.JOINED,
|
||||
),
|
||||
aNotJoinedRoom,
|
||||
),
|
||||
paginateResult = { Result.success(Unit) },
|
||||
)
|
||||
val presenter = createSpacePresenter(
|
||||
spaceRoomList = fakeSpaceRoomList,
|
||||
joinRoom = FakeJoinRoom(
|
||||
lambda = joinRoom,
|
||||
),
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
val state = awaitItem()
|
||||
assertThat(state.joinActions[A_ROOM_ID_2]).isNull()
|
||||
state.eventSink(SpaceEvents.Join(aNotJoinedRoom))
|
||||
val joiningState = awaitItem()
|
||||
assertThat(joiningState.joinActions[A_ROOM_ID_2]).isEqualTo(AsyncAction.Loading)
|
||||
// Let the joinRoom call complete
|
||||
advanceUntilIdle()
|
||||
runCurrent()
|
||||
// The room is joined
|
||||
fakeSpaceRoomList.emitSpaceRooms(
|
||||
listOf(
|
||||
aSpaceRoom(
|
||||
roomId = A_ROOM_ID,
|
||||
state = CurrentUserMembership.JOINED,
|
||||
),
|
||||
aNotJoinedRoom.copy(state = CurrentUserMembership.JOINED),
|
||||
)
|
||||
)
|
||||
skipItems(1)
|
||||
val joinedState = awaitItem()
|
||||
// Joined room is removed from the join actions
|
||||
assertThat(joinedState.joinActions).doesNotContainKey(A_ROOM_ID_2)
|
||||
joinRoom.assertions().isCalledOnce().with(
|
||||
value(A_ROOM_ID_2.toRoomIdOrAlias()),
|
||||
value(serverNames),
|
||||
value(AnalyticsJoinedRoom.Trigger.SpaceHierarchy),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - join a room failure`() = runTest {
|
||||
val aNotJoinedRoom = aSpaceRoom(
|
||||
roomId = A_ROOM_ID_2,
|
||||
state = null,
|
||||
)
|
||||
val fakeSpaceRoomList = FakeSpaceRoomList(
|
||||
initialSpaceRoomsValue = listOf(
|
||||
aSpaceRoom(
|
||||
roomId = A_ROOM_ID,
|
||||
state = CurrentUserMembership.JOINED,
|
||||
),
|
||||
aNotJoinedRoom,
|
||||
),
|
||||
paginateResult = { Result.success(Unit) },
|
||||
)
|
||||
val presenter = createSpacePresenter(
|
||||
spaceRoomList = fakeSpaceRoomList,
|
||||
joinRoom = FakeJoinRoom(
|
||||
lambda = { _, _, _ -> Result.failure(AN_EXCEPTION) },
|
||||
),
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
val state = awaitItem()
|
||||
assertThat(state.joinActions[A_ROOM_ID_2]).isNull()
|
||||
state.eventSink(SpaceEvents.Join(aNotJoinedRoom))
|
||||
val joiningState = awaitItem()
|
||||
assertThat(joiningState.joinActions[A_ROOM_ID_2]).isEqualTo(AsyncAction.Loading)
|
||||
val errorState = awaitItem()
|
||||
// Joined room is removed from the join actions
|
||||
assertThat(errorState.joinActions[A_ROOM_ID_2]!!.isFailure()).isTrue()
|
||||
// Clear error
|
||||
errorState.eventSink(SpaceEvents.ClearFailures)
|
||||
val clearedState = awaitItem()
|
||||
assertThat(clearedState.joinActions[A_ROOM_ID_2]).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - topic viewer state`() = runTest {
|
||||
val paginateResult = lambdaRecorder<Result<Unit>> {
|
||||
Result.success(Unit)
|
||||
}
|
||||
val spaceRoomList = FakeSpaceRoomList(paginateResult = paginateResult)
|
||||
val presenter = createSpacePresenter(spaceRoomList = spaceRoomList)
|
||||
presenter.test {
|
||||
val state = awaitItem()
|
||||
assertThat(state.topicViewerState).isEqualTo(TopicViewerState.Hidden)
|
||||
advanceUntilIdle()
|
||||
state.eventSink(SpaceEvents.ShowTopicViewer("topic"))
|
||||
assertThat(awaitItem().topicViewerState).isEqualTo(TopicViewerState.Shown("topic"))
|
||||
state.eventSink(SpaceEvents.HideTopicViewer)
|
||||
assertThat(awaitItem().topicViewerState).isEqualTo(TopicViewerState.Hidden)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - accept invite is transmitted to acceptDeclineInviteState`() {
|
||||
`invite action is transmitted to acceptDeclineInviteState`(
|
||||
acceptInvite = true,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - decline invite is transmitted to acceptDeclineInviteState`() {
|
||||
`invite action is transmitted to acceptDeclineInviteState`(
|
||||
acceptInvite = false,
|
||||
)
|
||||
}
|
||||
|
||||
private fun `invite action is transmitted to acceptDeclineInviteState`(
|
||||
acceptInvite: Boolean,
|
||||
) = runTest {
|
||||
val eventRecorder = EventsRecorder<AcceptDeclineInviteEvents>()
|
||||
val anInvitedRoom = aSpaceRoom(
|
||||
roomId = A_ROOM_ID_2,
|
||||
state = CurrentUserMembership.INVITED,
|
||||
)
|
||||
val fakeSpaceRoomList = FakeSpaceRoomList(
|
||||
initialSpaceRoomsValue = listOf(
|
||||
aSpaceRoom(
|
||||
roomId = A_ROOM_ID,
|
||||
state = CurrentUserMembership.JOINED,
|
||||
),
|
||||
anInvitedRoom,
|
||||
),
|
||||
paginateResult = { Result.success(Unit) },
|
||||
)
|
||||
val presenter = createSpacePresenter(
|
||||
spaceRoomList = fakeSpaceRoomList,
|
||||
acceptDeclineInvitePresenter = {
|
||||
anAcceptDeclineInviteState(
|
||||
eventSink = eventRecorder,
|
||||
)
|
||||
},
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
val state = awaitItem()
|
||||
assertThat(state.joinActions[A_ROOM_ID_2]).isNull()
|
||||
if (acceptInvite) {
|
||||
state.eventSink(SpaceEvents.AcceptInvite(anInvitedRoom))
|
||||
eventRecorder.assertSingle(
|
||||
AcceptDeclineInviteEvents.AcceptInvite(
|
||||
invite = anInvitedRoom.toInviteData(),
|
||||
)
|
||||
)
|
||||
} else {
|
||||
state.eventSink(SpaceEvents.DeclineInvite(anInvitedRoom))
|
||||
eventRecorder.assertSingle(
|
||||
AcceptDeclineInviteEvents.DeclineInvite(
|
||||
invite = anInvitedRoom.toInviteData(),
|
||||
shouldConfirm = true,
|
||||
blockUser = false,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun TestScope.createSpacePresenter(
|
||||
client: MatrixClient = FakeMatrixClient(),
|
||||
spaceRoomList: SpaceRoomList = FakeSpaceRoomList(),
|
||||
seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(),
|
||||
joinRoom: JoinRoom = FakeJoinRoom(
|
||||
lambda = { _, _, _ -> Result.success(Unit) },
|
||||
),
|
||||
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
|
||||
): SpacePresenter {
|
||||
return SpacePresenter(
|
||||
client = client,
|
||||
spaceRoomList = spaceRoomList,
|
||||
seenInvitesStore = seenInvitesStore,
|
||||
joinRoom = joinRoom,
|
||||
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
|
||||
sessionCoroutineScope = backgroundScope,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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.space.impl.root
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_3
|
||||
import org.junit.Test
|
||||
|
||||
class SpaceStateTest {
|
||||
@Test
|
||||
fun `test default state`() {
|
||||
val state = aSpaceState()
|
||||
assertThat(state.hasAnyFailure).isFalse()
|
||||
assertThat(state.isJoining(A_ROOM_ID)).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test has failure`() {
|
||||
val state = aSpaceState(
|
||||
joinActions = mapOf(
|
||||
A_ROOM_ID to AsyncAction.Uninitialized,
|
||||
A_ROOM_ID_2 to AsyncAction.Failure(AN_EXCEPTION),
|
||||
A_ROOM_ID_3 to AsyncAction.Success(Unit),
|
||||
)
|
||||
)
|
||||
assertThat(state.hasAnyFailure).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test isJoining`() {
|
||||
val state = aSpaceState(
|
||||
joinActions = mapOf(
|
||||
A_ROOM_ID to AsyncAction.Loading,
|
||||
)
|
||||
)
|
||||
assertThat(state.isJoining(A_ROOM_ID)).isTrue()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* 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.space.impl.root
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_TOPIC
|
||||
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
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.clickOn
|
||||
import io.element.android.tests.testutils.ensureCalledOnce
|
||||
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
||||
import io.element.android.tests.testutils.pressBack
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SpaceViewTest {
|
||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `clicking on back invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<SpaceEvents>(expectEvents = false)
|
||||
ensureCalledOnce {
|
||||
rule.setSpaceView(
|
||||
aSpaceState(
|
||||
hasMoreToLoad = false,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onBackClick = it,
|
||||
)
|
||||
rule.pressBack()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on a room name invokes the expected callback`() {
|
||||
val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, displayName = A_ROOM_NAME)
|
||||
val eventsRecorder = EventsRecorder<SpaceEvents>(expectEvents = false)
|
||||
ensureCalledOnceWithParam(aSpaceRoom) {
|
||||
rule.setSpaceView(
|
||||
aSpaceState(
|
||||
children = listOf(aSpaceRoom),
|
||||
hasMoreToLoad = false,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onRoomClick = it,
|
||||
)
|
||||
rule.onNodeWithText(A_ROOM_NAME).performClick()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on Join room emits the expected Event`() {
|
||||
val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, state = null)
|
||||
val eventsRecorder = EventsRecorder<SpaceEvents>()
|
||||
rule.setSpaceView(
|
||||
aSpaceState(
|
||||
children = listOf(aSpaceRoom),
|
||||
hasMoreToLoad = false,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_join)
|
||||
eventsRecorder.assertSingle(SpaceEvents.Join(aSpaceRoom))
|
||||
}
|
||||
|
||||
@Config(qualifiers = "h1024dp")
|
||||
@Test
|
||||
fun `clicking on accept invite emits the expected Event`() {
|
||||
val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, state = CurrentUserMembership.INVITED)
|
||||
val eventsRecorder = EventsRecorder<SpaceEvents>()
|
||||
rule.setSpaceView(
|
||||
aSpaceState(
|
||||
hasMoreToLoad = false,
|
||||
children = listOf(aSpaceRoom),
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_accept)
|
||||
eventsRecorder.assertSingle(SpaceEvents.AcceptInvite(aSpaceRoom))
|
||||
}
|
||||
|
||||
@Config(qualifiers = "h1024dp")
|
||||
@Test
|
||||
fun `clicking on decline invite emits the expected Event`() {
|
||||
val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, state = CurrentUserMembership.INVITED)
|
||||
val eventsRecorder = EventsRecorder<SpaceEvents>()
|
||||
rule.setSpaceView(
|
||||
aSpaceState(
|
||||
hasMoreToLoad = false,
|
||||
children = listOf(aSpaceRoom),
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_decline)
|
||||
eventsRecorder.assertSingle(SpaceEvents.DeclineInvite(aSpaceRoom))
|
||||
}
|
||||
|
||||
@Config(qualifiers = "h1024dp")
|
||||
@Test
|
||||
fun `clicking on topic emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<SpaceEvents>()
|
||||
rule.setSpaceView(
|
||||
aSpaceState(
|
||||
parentSpace = aSpaceRoom(topic = A_ROOM_TOPIC),
|
||||
hasMoreToLoad = false,
|
||||
eventSink = eventsRecorder,
|
||||
)
|
||||
)
|
||||
rule.onNodeWithText(A_ROOM_TOPIC).performClick()
|
||||
eventsRecorder.assertSingle(SpaceEvents.ShowTopicViewer(A_ROOM_TOPIC))
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSpaceView(
|
||||
state: SpaceState,
|
||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||
onRoomClick: (SpaceRoom) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onShareSpace: () -> Unit = EnsureNeverCalled(),
|
||||
onLeaveSpaceClick: () -> Unit = EnsureNeverCalled(),
|
||||
onDetailsClick: () -> Unit = EnsureNeverCalled(),
|
||||
onViewMembersClick: () -> Unit = EnsureNeverCalled(),
|
||||
acceptDeclineInviteView: @Composable () -> Unit = {},
|
||||
) {
|
||||
setContent {
|
||||
SpaceView(
|
||||
state = state,
|
||||
onBackClick = onBackClick,
|
||||
onRoomClick = onRoomClick,
|
||||
onShareSpace = onShareSpace,
|
||||
onLeaveSpaceClick = onLeaveSpaceClick,
|
||||
onDetailsClick = onDetailsClick,
|
||||
onViewMembersClick = onViewMembersClick,
|
||||
acceptDeclineInviteView = acceptDeclineInviteView,
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user