Merge pull request #5979 from element-hq/feature/fga/space_members_access
Change Room’s Access to/from Space members
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
package io.element.android.features.securityandprivacy.impl
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.Lifecycle
|
||||
@@ -24,6 +25,7 @@ import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyEntryPoint
|
||||
import io.element.android.features.securityandprivacy.api.securityAndPrivacyPermissions
|
||||
import io.element.android.features.securityandprivacy.impl.editroomaddress.EditRoomAddressNode
|
||||
import io.element.android.features.securityandprivacy.impl.manageauthorizedspaces.ManageAuthorizedSpacesNode
|
||||
import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyNode
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
@@ -58,10 +60,15 @@ class SecurityAndPrivacyFlowNode(
|
||||
|
||||
@Parcelize
|
||||
data object EditRoomAddress : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object ManageAuthorizedSpaces : NavTarget
|
||||
}
|
||||
|
||||
private val callback: SecurityAndPrivacyEntryPoint.Callback = callback()
|
||||
private val navigator = BackstackSecurityAndPrivacyNavigator(callback, backstack)
|
||||
|
||||
@VisibleForTesting
|
||||
val navigator = BackstackSecurityAndPrivacyNavigator(callback, backstack)
|
||||
|
||||
override fun onBuilt() {
|
||||
super.onBuilt()
|
||||
@@ -89,6 +96,9 @@ class SecurityAndPrivacyFlowNode(
|
||||
NavTarget.EditRoomAddress -> {
|
||||
createNode<EditRoomAddressNode>(buildContext, plugins = listOf(navigator))
|
||||
}
|
||||
NavTarget.ManageAuthorizedSpaces -> {
|
||||
createNode<ManageAuthorizedSpacesNode>(buildContext, plugins = listOf(navigator))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ interface SecurityAndPrivacyNavigator : Plugin {
|
||||
fun onDone()
|
||||
fun openEditRoomAddress()
|
||||
fun closeEditRoomAddress()
|
||||
fun openManageAuthorizedSpaces()
|
||||
fun closeManageAuthorizedSpaces()
|
||||
}
|
||||
|
||||
class BackstackSecurityAndPrivacyNavigator(
|
||||
@@ -35,4 +37,12 @@ class BackstackSecurityAndPrivacyNavigator(
|
||||
override fun closeEditRoomAddress() {
|
||||
backStack.pop()
|
||||
}
|
||||
|
||||
override fun openManageAuthorizedSpaces() {
|
||||
backStack.push(SecurityAndPrivacyFlowNode.NavTarget.ManageAuthorizedSpaces)
|
||||
}
|
||||
|
||||
override fun closeManageAuthorizedSpaces() {
|
||||
backStack.pop()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations 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.securityandprivacy.impl.manageauthorizedspaces
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
sealed interface ManageAuthorizedSpacesEvent {
|
||||
data object Cancel : ManageAuthorizedSpacesEvent
|
||||
data object Done : ManageAuthorizedSpacesEvent
|
||||
data class ToggleSpace(val roomId: RoomId) : ManageAuthorizedSpacesEvent
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations 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.securityandprivacy.impl.manageauthorizedspaces
|
||||
|
||||
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 com.bumble.appyx.core.plugin.plugins
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.libraries.architecture.appyx.launchMolecule
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
@AssistedInject
|
||||
class ManageAuthorizedSpacesNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
presenter: ManageAuthorizedSpacesPresenter,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
private val stateFlow = launchMolecule { presenter.present() }
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state by stateFlow.collectAsState()
|
||||
ManageAuthorizedSpacesView(
|
||||
state = state,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations 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.securityandprivacy.impl.manageauthorizedspaces
|
||||
|
||||
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 kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Inject
|
||||
class ManageAuthorizedSpacesPresenter(
|
||||
private val spaceSelectionStateHolder: SpaceSelectionStateHolder,
|
||||
) : Presenter<ManageAuthorizedSpacesState> {
|
||||
@Composable
|
||||
override fun present(): ManageAuthorizedSpacesState {
|
||||
val spaceSelectionState by spaceSelectionStateHolder.state.collectAsState()
|
||||
fun handleEvent(event: ManageAuthorizedSpacesEvent) {
|
||||
when (event) {
|
||||
is ManageAuthorizedSpacesEvent.ToggleSpace -> {
|
||||
val currentSelectedIds = spaceSelectionState.selectedSpaceIds
|
||||
val newSelectedIds = if (currentSelectedIds.contains(event.roomId)) {
|
||||
currentSelectedIds.minus(event.roomId).toImmutableList()
|
||||
} else {
|
||||
currentSelectedIds.plus(event.roomId).toImmutableList()
|
||||
}
|
||||
spaceSelectionStateHolder.updateSelectedSpaceIds(newSelectedIds)
|
||||
}
|
||||
ManageAuthorizedSpacesEvent.Done -> {
|
||||
spaceSelectionStateHolder.setCompletion(SpaceSelectionState.Completion.Completed)
|
||||
}
|
||||
ManageAuthorizedSpacesEvent.Cancel -> {
|
||||
spaceSelectionStateHolder.setCompletion(SpaceSelectionState.Completion.Cancelled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ManageAuthorizedSpacesState(
|
||||
selectableSpaces = spaceSelectionState.selectableSpaces,
|
||||
unknownSpaceIds = spaceSelectionState.unknownSpaceIds,
|
||||
selectedIds = spaceSelectionState.selectedSpaceIds,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations 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.securityandprivacy.impl.manageauthorizedspaces
|
||||
|
||||
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.ImmutableSet
|
||||
|
||||
data class ManageAuthorizedSpacesState(
|
||||
val selectableSpaces: ImmutableSet<SpaceRoom>,
|
||||
val unknownSpaceIds: ImmutableList<RoomId>,
|
||||
val selectedIds: ImmutableList<RoomId>,
|
||||
val eventSink: (ManageAuthorizedSpacesEvent) -> Unit
|
||||
) {
|
||||
val isDoneButtonEnabled = selectedIds.isNotEmpty()
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations 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.securityandprivacy.impl.manageauthorizedspaces
|
||||
|
||||
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
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
|
||||
open class ManageAuthorizedSpacesStateProvider : PreviewParameterProvider<ManageAuthorizedSpacesState> {
|
||||
override val values: Sequence<ManageAuthorizedSpacesState>
|
||||
get() = sequenceOf(
|
||||
aManageAuthorizedSpacesState(),
|
||||
aManageAuthorizedSpacesState(
|
||||
unknownSpaceIds = listOf(aRoomId(99))
|
||||
),
|
||||
aManageAuthorizedSpacesState(
|
||||
selectedIds = listOf(aRoomId(1), aRoomId(3)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun aRoomId(index: Int) = RoomId("!roomId$index:matrix.org")
|
||||
|
||||
private fun aSpaceRoomList(count: Int): List<SpaceRoom> {
|
||||
return (1..count).map { index ->
|
||||
aSpaceRoom(
|
||||
roomId = aRoomId(index),
|
||||
displayName = "Space $index",
|
||||
canonicalAlias = if (index % 2 == 0) {
|
||||
RoomAlias("#space$index:matrix.org")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun aManageAuthorizedSpacesState(
|
||||
selectableSpaces: List<SpaceRoom> = aSpaceRoomList(5),
|
||||
unknownSpaceIds: List<RoomId> = emptyList(),
|
||||
selectedIds: List<RoomId> = emptyList(),
|
||||
eventSink: (ManageAuthorizedSpacesEvent) -> Unit = {},
|
||||
) = ManageAuthorizedSpacesState(
|
||||
selectableSpaces = selectableSpaces.toImmutableSet(),
|
||||
unknownSpaceIds = unknownSpaceIds.toImmutableList(),
|
||||
selectedIds = selectedIds.toImmutableList(),
|
||||
eventSink = eventSink,
|
||||
)
|
||||
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations 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.securityandprivacy.impl.manageauthorizedspaces
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
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.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.securityandprivacy.impl.R
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||
import io.element.android.libraries.designsystem.components.BigIcon
|
||||
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.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||
import io.element.android.libraries.designsystem.theme.components.ListSectionHeader
|
||||
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.ui.model.getAvatarData
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
// Figma design: https://www.figma.com/design/kcnHxunG1LDWXsJhaNuiHz/ER-145--Spaces-on-Element-X?node-id=6361-86274&m=dev
|
||||
@Composable
|
||||
fun ManageAuthorizedSpacesView(
|
||||
state: ManageAuthorizedSpacesState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun onCancel() {
|
||||
state.eventSink(ManageAuthorizedSpacesEvent.Cancel)
|
||||
}
|
||||
|
||||
fun onDone() {
|
||||
state.eventSink(ManageAuthorizedSpacesEvent.Done)
|
||||
}
|
||||
|
||||
BackHandler(onBack = ::onCancel)
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
ManageAuthorizedSpacesTopBar(
|
||||
onBackClick = ::onCancel,
|
||||
onDoneClick = ::onDone,
|
||||
isDoneButtonEnabled = state.isDoneButtonEnabled
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
LazyColumn(
|
||||
modifier = Modifier.padding(padding)
|
||||
) {
|
||||
headerItem()
|
||||
item {
|
||||
ListSectionHeader(
|
||||
title = stringResource(R.string.screen_manage_authorized_spaces_your_spaces_section_title),
|
||||
hasDivider = false,
|
||||
)
|
||||
}
|
||||
items(items = state.selectableSpaces.toList()) { space ->
|
||||
CheckableSpaceListItem(
|
||||
headlineText = space.displayName,
|
||||
supportingText = space.canonicalAlias?.value,
|
||||
avatarData = space.getAvatarData(AvatarSize.SpaceMember),
|
||||
checked = state.selectedIds.contains(space.roomId),
|
||||
onCheckedChange = { _ ->
|
||||
state.eventSink(
|
||||
ManageAuthorizedSpacesEvent.ToggleSpace(space.roomId)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
if (state.unknownSpaceIds.isNotEmpty()) {
|
||||
item {
|
||||
ListSectionHeader(
|
||||
title = stringResource(R.string.screen_manage_authorized_spaces_unknown_spaces_section_title),
|
||||
hasDivider = true,
|
||||
)
|
||||
}
|
||||
items(items = state.unknownSpaceIds) {
|
||||
CheckableSpaceListItem(
|
||||
headlineText = stringResource(R.string.screen_manage_authorized_spaces_unknown_space),
|
||||
supportingText = it.value,
|
||||
avatarData = null,
|
||||
checked = state.selectedIds.contains(it),
|
||||
onCheckedChange = { _ ->
|
||||
state.eventSink(
|
||||
ManageAuthorizedSpacesEvent.ToggleSpace(it)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.headerItem() {
|
||||
item(key = "header", contentType = "header") {
|
||||
IconTitleSubtitleMolecule(
|
||||
modifier = Modifier.padding(
|
||||
vertical = 16.dp,
|
||||
horizontal = 24.dp
|
||||
),
|
||||
title = stringResource(R.string.screen_manage_authorized_spaces_header),
|
||||
subTitle = null,
|
||||
iconStyle = BigIcon.Style.Default(
|
||||
vectorIcon = CompoundIcons.SpaceSolid(),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CheckableSpaceListItem(
|
||||
headlineText: String,
|
||||
supportingText: String?,
|
||||
avatarData: AvatarData?,
|
||||
checked: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(text = headlineText)
|
||||
},
|
||||
supportingContent = supportingText?.let {
|
||||
@Composable {
|
||||
Text(text = supportingText)
|
||||
}
|
||||
},
|
||||
leadingContent = avatarData?.let {
|
||||
ListItemContent.Custom {
|
||||
Avatar(
|
||||
avatarData = avatarData,
|
||||
avatarType = AvatarType.Space(),
|
||||
)
|
||||
}
|
||||
},
|
||||
trailingContent = ListItemContent.Checkbox(
|
||||
checked = checked,
|
||||
enabled = enabled,
|
||||
),
|
||||
enabled = enabled,
|
||||
onClick = { onCheckedChange(!checked) },
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ManageAuthorizedSpacesTopBar(
|
||||
isDoneButtonEnabled: Boolean,
|
||||
onBackClick: () -> Unit,
|
||||
onDoneClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
TopAppBar(
|
||||
modifier = modifier,
|
||||
titleStr = stringResource(R.string.screen_manage_authorized_spaces_title),
|
||||
navigationIcon = { BackButton(onClick = onBackClick) },
|
||||
actions = {
|
||||
TextButton(
|
||||
enabled = isDoneButtonEnabled,
|
||||
text = stringResource(CommonStrings.action_done),
|
||||
onClick = onDoneClick,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun ManageAuthorizedSpacesViewPreview(
|
||||
@PreviewParameter(ManageAuthorizedSpacesStateProvider::class) state: ManageAuthorizedSpacesState
|
||||
) = ElementPreview {
|
||||
ManageAuthorizedSpacesView(state = state)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations 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.securityandprivacy.impl.manageauthorizedspaces
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
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.ImmutableSet
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
data class SpaceSelectionState(
|
||||
val selectableSpaces: ImmutableSet<SpaceRoom>,
|
||||
val unknownSpaceIds: ImmutableList<RoomId>,
|
||||
val selectedSpaceIds: ImmutableList<RoomId>,
|
||||
val completion: Completion,
|
||||
) {
|
||||
enum class Completion {
|
||||
Initial,
|
||||
Completed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
companion object {
|
||||
val INITIAL = SpaceSelectionState(
|
||||
selectableSpaces = persistentSetOf(),
|
||||
unknownSpaceIds = persistentListOf(),
|
||||
selectedSpaceIds = persistentListOf(),
|
||||
completion = Completion.Initial,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Inject
|
||||
@SingleIn(RoomScope::class)
|
||||
class SpaceSelectionStateHolder {
|
||||
private val _state = MutableStateFlow(SpaceSelectionState.INITIAL)
|
||||
val state: StateFlow<SpaceSelectionState> = _state.asStateFlow()
|
||||
|
||||
fun update(transform: (SpaceSelectionState) -> SpaceSelectionState) {
|
||||
_state.update(transform)
|
||||
}
|
||||
|
||||
fun updateSelectedSpaceIds(selectedSpaceIds: ImmutableList<RoomId>) {
|
||||
update { it.copy(selectedSpaceIds = selectedSpaceIds) }
|
||||
}
|
||||
|
||||
fun setCompletion(completion: SpaceSelectionState.Completion) {
|
||||
update { it.copy(completion = completion) }
|
||||
}
|
||||
}
|
||||
@@ -10,10 +10,17 @@ package io.element.android.features.securityandprivacy.impl.root
|
||||
|
||||
sealed interface SecurityAndPrivacyEvent {
|
||||
data object EditRoomAddress : SecurityAndPrivacyEvent
|
||||
data object ManageAuthorizedSpaces : SecurityAndPrivacyEvent
|
||||
data object Save : SecurityAndPrivacyEvent
|
||||
data object Exit : SecurityAndPrivacyEvent
|
||||
data object DismissExitConfirmation : SecurityAndPrivacyEvent
|
||||
data class ChangeRoomAccess(val roomAccess: SecurityAndPrivacyRoomAccess) : SecurityAndPrivacyEvent
|
||||
|
||||
// Special case for "Space Members"
|
||||
data object SelectSpaceMemberAccess : SecurityAndPrivacyEvent
|
||||
|
||||
// Special case for "Ask to join with Space Members"
|
||||
data object SelectAskToJoinWithSpaceMembersAccess : SecurityAndPrivacyEvent
|
||||
data object ToggleEncryptionState : SecurityAndPrivacyEvent
|
||||
data object CancelEnableEncryption : SecurityAndPrivacyEvent
|
||||
data object ConfirmEnableEncryption : SecurityAndPrivacyEvent
|
||||
|
||||
@@ -15,6 +15,7 @@ import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -25,6 +26,8 @@ import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyPerm
|
||||
import io.element.android.features.securityandprivacy.api.securityAndPrivacyPermissions
|
||||
import io.element.android.features.securityandprivacy.impl.SecurityAndPrivacyNavigator
|
||||
import io.element.android.features.securityandprivacy.impl.editroomaddress.matchesServer
|
||||
import io.element.android.features.securityandprivacy.impl.manageauthorizedspaces.SpaceSelectionState
|
||||
import io.element.android.features.securityandprivacy.impl.manageauthorizedspaces.SpaceSelectionStateHolder
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
@@ -37,25 +40,35 @@ import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomInfo
|
||||
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
|
||||
import io.element.android.libraries.matrix.api.room.join.AllowRule
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState
|
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@AssistedInject
|
||||
class SecurityAndPrivacyPresenter(
|
||||
@Assisted private val navigator: SecurityAndPrivacyNavigator,
|
||||
private val spaceSelectionStateHolder: SpaceSelectionStateHolder,
|
||||
private val matrixClient: MatrixClient,
|
||||
private val room: JoinedRoom,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
) : Presenter<SecurityAndPrivacyState> {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(navigator: SecurityAndPrivacyNavigator): SecurityAndPrivacyPresenter
|
||||
fun create(
|
||||
navigator: SecurityAndPrivacyNavigator,
|
||||
): SecurityAndPrivacyPresenter
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -65,6 +78,10 @@ class SecurityAndPrivacyPresenter(
|
||||
val isKnockEnabled by remember {
|
||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock)
|
||||
}.collectAsState(false)
|
||||
val isSpaceSettingsEnabled by remember {
|
||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.SpaceSettings)
|
||||
}.collectAsState(false)
|
||||
|
||||
val saveAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||
val homeserverName = remember { matrixClient.userIdServerName() }
|
||||
val roomInfo by room.roomInfoFlow.collectAsState()
|
||||
@@ -86,7 +103,7 @@ class SecurityAndPrivacyPresenter(
|
||||
}
|
||||
}
|
||||
|
||||
var editedRoomAccess by remember(savedSettings.roomAccess) {
|
||||
val editedRoomAccess = remember(savedSettings.roomAccess) {
|
||||
mutableStateOf(savedSettings.roomAccess)
|
||||
}
|
||||
var editedHistoryVisibility by remember(savedSettings.historyVisibility) {
|
||||
@@ -99,13 +116,44 @@ class SecurityAndPrivacyPresenter(
|
||||
mutableStateOf(savedIsVisibleInRoomDirectory.value)
|
||||
}
|
||||
val editedSettings = SecurityAndPrivacySettings(
|
||||
roomAccess = editedRoomAccess,
|
||||
roomAccess = editedRoomAccess.value,
|
||||
isEncrypted = editedIsEncrypted,
|
||||
isVisibleInRoomDirectory = editedVisibleInRoomDirectory,
|
||||
historyVisibility = editedHistoryVisibility,
|
||||
address = savedSettings.address,
|
||||
)
|
||||
|
||||
val selectableJoinedSpaces by produceState(initialValue = persistentSetOf(), key1 = savedSettings.roomAccess.spaceIds()) {
|
||||
val joinedParentSpaces = matrixClient
|
||||
.spaceService
|
||||
.joinedParents(room.roomId)
|
||||
.getOrDefault(emptyList())
|
||||
|
||||
val nonParentJoinedSpaces = savedSettings.roomAccess
|
||||
.spaceIds()
|
||||
.mapNotNull { spaceId -> matrixClient.spaceService.getSpaceRoom(spaceId) }
|
||||
|
||||
value = (joinedParentSpaces + nonParentJoinedSpaces).toImmutableSet()
|
||||
}
|
||||
|
||||
val spaceSelectionMode by remember {
|
||||
derivedStateOf {
|
||||
getSpaceSelectionMode(selectableJoinedSpaces, savedSettings.roomAccess)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(selectableJoinedSpaces, savedSettings.roomAccess) {
|
||||
val unknownSpaceIds = savedSettings.roomAccess.spaceIds().filter { spaceId ->
|
||||
selectableJoinedSpaces.none { it.roomId == spaceId }
|
||||
}.toImmutableList()
|
||||
spaceSelectionStateHolder.update { state ->
|
||||
state.copy(
|
||||
selectableSpaces = selectableJoinedSpaces,
|
||||
unknownSpaceIds = unknownSpaceIds,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var showEnableEncryptionConfirmation by remember(savedSettings.isEncrypted) { mutableStateOf(false) }
|
||||
val permissions by room.permissionsAsState(SecurityAndPrivacyPermissions.DEFAULT) { perms ->
|
||||
perms.securityAndPrivacyPermissions()
|
||||
@@ -122,7 +170,7 @@ class SecurityAndPrivacyPresenter(
|
||||
)
|
||||
}
|
||||
is SecurityAndPrivacyEvent.ChangeRoomAccess -> {
|
||||
editedRoomAccess = event.roomAccess
|
||||
editedRoomAccess.value = event.roomAccess
|
||||
}
|
||||
is SecurityAndPrivacyEvent.ToggleEncryptionState -> {
|
||||
if (editedIsEncrypted) {
|
||||
@@ -161,6 +209,27 @@ class SecurityAndPrivacyPresenter(
|
||||
SecurityAndPrivacyEvent.DismissExitConfirmation -> {
|
||||
saveAction.value = AsyncAction.Uninitialized
|
||||
}
|
||||
SecurityAndPrivacyEvent.ManageAuthorizedSpaces -> coroutineScope.launch {
|
||||
handleMultipleSelection(
|
||||
savedAccess = savedSettings.roomAccess,
|
||||
editedRoomAccess = editedRoomAccess,
|
||||
forKnockRestricted = editedRoomAccess.value is SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember
|
||||
)
|
||||
}
|
||||
SecurityAndPrivacyEvent.SelectSpaceMemberAccess -> coroutineScope.launch {
|
||||
handleSpaceMemberAccessSelection(
|
||||
spaceSelectionMode = spaceSelectionMode,
|
||||
savedAccess = savedSettings.roomAccess,
|
||||
editedAccess = editedRoomAccess,
|
||||
)
|
||||
}
|
||||
SecurityAndPrivacyEvent.SelectAskToJoinWithSpaceMembersAccess -> coroutineScope.launch {
|
||||
handleAskToJoinWithSpaceMembersAccessSelection(
|
||||
spaceSelectionMode = spaceSelectionMode,
|
||||
savedAccess = savedSettings.roomAccess,
|
||||
editedAccess = editedRoomAccess,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,13 +248,16 @@ class SecurityAndPrivacyPresenter(
|
||||
saveAction = saveAction.value,
|
||||
permissions = permissions,
|
||||
isSpace = roomInfo.isSpace,
|
||||
isSpaceSettingsEnabled = isSpaceSettingsEnabled,
|
||||
selectableJoinedSpaces = selectableJoinedSpaces,
|
||||
spaceSelectionMode = spaceSelectionMode,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
|
||||
// Revert changes that the user is not allowed to make anymore
|
||||
LaunchedEffect(permissions, state.editedSettings.roomAccess) {
|
||||
if (!state.showRoomAccessSection) {
|
||||
editedRoomAccess = savedSettings.roomAccess
|
||||
editedRoomAccess.value = savedSettings.roomAccess
|
||||
}
|
||||
if (!state.showEncryptionSection) {
|
||||
editedIsEncrypted = savedSettings.isEncrypted
|
||||
@@ -202,6 +274,110 @@ class SecurityAndPrivacyPresenter(
|
||||
return state
|
||||
}
|
||||
|
||||
private suspend fun handleSpaceMemberAccessSelection(
|
||||
spaceSelectionMode: SpaceSelectionMode,
|
||||
savedAccess: SecurityAndPrivacyRoomAccess,
|
||||
editedAccess: MutableState<SecurityAndPrivacyRoomAccess>,
|
||||
) {
|
||||
if (editedAccess.value is SecurityAndPrivacyRoomAccess.SpaceMember) {
|
||||
return
|
||||
}
|
||||
when (spaceSelectionMode) {
|
||||
is SpaceSelectionMode.None -> Unit
|
||||
is SpaceSelectionMode.Multiple -> handleMultipleSelection(
|
||||
savedAccess = savedAccess,
|
||||
editedRoomAccess = editedAccess,
|
||||
forKnockRestricted = false,
|
||||
)
|
||||
is SpaceSelectionMode.Single -> {
|
||||
val newRoomAccess = SecurityAndPrivacyRoomAccess.SpaceMember(
|
||||
spaceIds = persistentListOf(spaceSelectionMode.spaceId)
|
||||
)
|
||||
editedAccess.value = newRoomAccess
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleAskToJoinWithSpaceMembersAccessSelection(
|
||||
spaceSelectionMode: SpaceSelectionMode,
|
||||
savedAccess: SecurityAndPrivacyRoomAccess,
|
||||
editedAccess: MutableState<SecurityAndPrivacyRoomAccess>,
|
||||
) {
|
||||
if (editedAccess.value is SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember) {
|
||||
return
|
||||
}
|
||||
when (spaceSelectionMode) {
|
||||
is SpaceSelectionMode.None -> Unit
|
||||
is SpaceSelectionMode.Multiple -> handleMultipleSelection(
|
||||
savedAccess = savedAccess,
|
||||
editedRoomAccess = editedAccess,
|
||||
forKnockRestricted = true,
|
||||
)
|
||||
is SpaceSelectionMode.Single -> {
|
||||
val newRoomAccess = SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember(
|
||||
spaceIds = persistentListOf(spaceSelectionMode.spaceId)
|
||||
)
|
||||
editedAccess.value = newRoomAccess
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleMultipleSelection(
|
||||
savedAccess: SecurityAndPrivacyRoomAccess,
|
||||
editedRoomAccess: MutableState<SecurityAndPrivacyRoomAccess>,
|
||||
forKnockRestricted: Boolean
|
||||
) {
|
||||
val initialSelection = when (val currentRoomAccess = editedRoomAccess.value) {
|
||||
is SecurityAndPrivacyRoomAccess.SpaceMember -> currentRoomAccess.spaceIds
|
||||
is SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember -> currentRoomAccess.spaceIds
|
||||
else -> savedAccess.spaceIds()
|
||||
}
|
||||
spaceSelectionStateHolder.update { state ->
|
||||
state.copy(selectedSpaceIds = initialSelection, completion = SpaceSelectionState.Completion.Initial)
|
||||
}
|
||||
navigator.openManageAuthorizedSpaces()
|
||||
val newState = spaceSelectionStateHolder.state.first { it.completion != SpaceSelectionState.Completion.Initial }
|
||||
when (newState.completion) {
|
||||
SpaceSelectionState.Completion.Initial -> Unit
|
||||
SpaceSelectionState.Completion.Cancelled -> {
|
||||
navigator.closeManageAuthorizedSpaces()
|
||||
}
|
||||
SpaceSelectionState.Completion.Completed -> {
|
||||
val selectedIds = newState.selectedSpaceIds
|
||||
editedRoomAccess.value = if (forKnockRestricted) {
|
||||
SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember(spaceIds = selectedIds)
|
||||
} else {
|
||||
SecurityAndPrivacyRoomAccess.SpaceMember(spaceIds = selectedIds)
|
||||
}
|
||||
navigator.closeManageAuthorizedSpaces()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSpaceSelectionMode(
|
||||
selectableJoinedSpaces: Set<SpaceRoom>,
|
||||
savedAccess: SecurityAndPrivacyRoomAccess,
|
||||
): SpaceSelectionMode {
|
||||
val selectableSpacesCount = (selectableJoinedSpaces.map { it.roomId } + savedAccess.spaceIds()).toSet().size
|
||||
return when {
|
||||
selectableSpacesCount == 0 -> SpaceSelectionMode.None
|
||||
selectableSpacesCount > 1 -> SpaceSelectionMode.Multiple
|
||||
else -> {
|
||||
val joinedSpace = selectableJoinedSpaces.firstOrNull()
|
||||
if (joinedSpace != null) {
|
||||
SpaceSelectionMode.Single(joinedSpace.roomId, joinedSpace)
|
||||
} else {
|
||||
val spaceId = savedAccess.spaceIds().firstOrNull()
|
||||
if (spaceId == null) {
|
||||
SpaceSelectionMode.None
|
||||
} else {
|
||||
SpaceSelectionMode.Single(spaceId, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.isRoomVisibleInRoomDirectory(isRoomVisible: MutableState<AsyncData<Boolean>>) = launch {
|
||||
isRoomVisible.runUpdatingState {
|
||||
room.getRoomVisibility().map { it == RoomVisibility.Public }
|
||||
@@ -242,6 +418,7 @@ class SecurityAndPrivacyPresenter(
|
||||
// the room should be automatically made invisible (private) in the room directory.
|
||||
val editedIsVisibleInRoomDirectory = when (editedSettings.roomAccess) {
|
||||
SecurityAndPrivacyRoomAccess.AskToJoin,
|
||||
is SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember,
|
||||
SecurityAndPrivacyRoomAccess.Anyone -> editedSettings.isVisibleInRoomDirectory.dataOrNull()
|
||||
else -> false
|
||||
}
|
||||
@@ -279,8 +456,19 @@ class SecurityAndPrivacyPresenter(
|
||||
private fun JoinRule?.map(): SecurityAndPrivacyRoomAccess {
|
||||
return when (this) {
|
||||
JoinRule.Public -> SecurityAndPrivacyRoomAccess.Anyone
|
||||
JoinRule.Knock, is JoinRule.KnockRestricted -> SecurityAndPrivacyRoomAccess.AskToJoin
|
||||
is JoinRule.Restricted -> SecurityAndPrivacyRoomAccess.SpaceMember
|
||||
JoinRule.Knock -> SecurityAndPrivacyRoomAccess.AskToJoin
|
||||
is JoinRule.KnockRestricted -> SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember(
|
||||
spaceIds = this.rules
|
||||
.filterIsInstance<AllowRule.RoomMembership>()
|
||||
.map { it.roomId }
|
||||
.toImmutableList()
|
||||
)
|
||||
is JoinRule.Restricted -> SecurityAndPrivacyRoomAccess.SpaceMember(
|
||||
spaceIds = this.rules
|
||||
.filterIsInstance<AllowRule.RoomMembership>()
|
||||
.map { it.roomId }
|
||||
.toImmutableList()
|
||||
)
|
||||
JoinRule.Invite -> SecurityAndPrivacyRoomAccess.InviteOnly
|
||||
// All other cases are not supported so we default to InviteOnly
|
||||
is JoinRule.Custom,
|
||||
@@ -294,8 +482,12 @@ private fun SecurityAndPrivacyRoomAccess.map(): JoinRule? {
|
||||
SecurityAndPrivacyRoomAccess.Anyone -> JoinRule.Public
|
||||
SecurityAndPrivacyRoomAccess.AskToJoin -> JoinRule.Knock
|
||||
SecurityAndPrivacyRoomAccess.InviteOnly -> JoinRule.Private
|
||||
// SpaceMember can't be selected in the ui
|
||||
SecurityAndPrivacyRoomAccess.SpaceMember -> null
|
||||
is SecurityAndPrivacyRoomAccess.SpaceMember -> JoinRule.Restricted(
|
||||
rules = this.spaceIds.map { AllowRule.RoomMembership(it) }.toImmutableList()
|
||||
)
|
||||
is SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember -> JoinRule.KnockRestricted(
|
||||
rules = this.spaceIds.map { AllowRule.RoomMembership(it) }.toImmutableList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,9 +8,17 @@
|
||||
|
||||
package io.element.android.features.securityandprivacy.impl.root
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyPermissions
|
||||
import io.element.android.features.securityandprivacy.impl.R
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
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.ImmutableSet
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
data class SecurityAndPrivacyState(
|
||||
@@ -20,12 +28,42 @@ data class SecurityAndPrivacyState(
|
||||
val editedSettings: SecurityAndPrivacySettings,
|
||||
val homeserverName: String,
|
||||
val showEnableEncryptionConfirmation: Boolean,
|
||||
val isKnockEnabled: Boolean,
|
||||
private val isKnockEnabled: Boolean,
|
||||
private val isSpaceSettingsEnabled: Boolean,
|
||||
val saveAction: AsyncAction<Unit>,
|
||||
val isSpace: Boolean,
|
||||
private val permissions: SecurityAndPrivacyPermissions,
|
||||
private val selectableJoinedSpaces: ImmutableSet<SpaceRoom>,
|
||||
private val spaceSelectionMode: SpaceSelectionMode,
|
||||
val eventSink: (SecurityAndPrivacyEvent) -> Unit
|
||||
) {
|
||||
val isSpaceMemberSelectable = isSpaceSettingsEnabled && spaceSelectionMode != SpaceSelectionMode.None
|
||||
|
||||
// Show SpaceMember option in two cases:
|
||||
// - SpaceMember is the current saved value
|
||||
// - SpaceMember option is selectable (ie. the FF is enabled and there is at least one space to select)
|
||||
val showSpaceMemberOption = savedSettings.roomAccess is SecurityAndPrivacyRoomAccess.SpaceMember || isSpaceMemberSelectable
|
||||
|
||||
val showManageSpaceFooter = spaceSelectionMode is SpaceSelectionMode.Multiple &&
|
||||
(editedSettings.roomAccess is SecurityAndPrivacyRoomAccess.SpaceMember ||
|
||||
editedSettings.roomAccess is SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember)
|
||||
|
||||
val isAskToJoinSelectable = isKnockEnabled
|
||||
|
||||
val isAskToJoinWithSpaceMembersSelectable = isAskToJoinSelectable && isSpaceMemberSelectable
|
||||
|
||||
// Show Ask to join option only when:
|
||||
// - AskToJoin is the current saved value (legacy), OR
|
||||
// - Knock FF enabled BUT (SpaceSettings FF disabled OR no spaces available)
|
||||
val showAskToJoinOption = savedSettings.roomAccess == SecurityAndPrivacyRoomAccess.AskToJoin ||
|
||||
isAskToJoinSelectable && !isAskToJoinWithSpaceMembersSelectable
|
||||
|
||||
// Show AskToJoinWithSpaceMember option when:
|
||||
// - It's the current saved value, OR
|
||||
// - Both FFs enabled AND spaces available
|
||||
val showAskToJoinWithSpaceMemberOption = savedSettings.roomAccess is SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember ||
|
||||
isAskToJoinWithSpaceMembersSelectable
|
||||
|
||||
val canBeSaved = savedSettings != editedSettings
|
||||
|
||||
// Logic is in https://github.com/element-hq/element-meta/issues/3029
|
||||
@@ -48,6 +86,40 @@ data class SecurityAndPrivacyState(
|
||||
|
||||
val showHistoryVisibilitySection = permissions.canChangeHistoryVisibility && !isSpace
|
||||
val showEncryptionSection = permissions.canChangeEncryption && !isSpace
|
||||
|
||||
@Composable
|
||||
fun spaceMemberDescription(): String {
|
||||
return if (isSpaceMemberSelectable) {
|
||||
when (spaceSelectionMode) {
|
||||
is SpaceSelectionMode.Single -> {
|
||||
val spaceName = spaceSelectionMode.spaceRoom?.displayName ?: spaceSelectionMode.spaceId.value
|
||||
stringResource(R.string.screen_security_and_privacy_room_access_space_members_option_single_parent_description, spaceName)
|
||||
}
|
||||
is SpaceSelectionMode.None,
|
||||
is SpaceSelectionMode.Multiple -> stringResource(
|
||||
R.string.screen_security_and_privacy_room_access_space_members_option_multiple_parents_description
|
||||
)
|
||||
}
|
||||
} else {
|
||||
stringResource(R.string.screen_security_and_privacy_room_access_space_members_option_unavailable_description)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun askToJoinWithSpaceMembersDescription(): String {
|
||||
return if (isAskToJoinWithSpaceMembersSelectable) {
|
||||
when (spaceSelectionMode) {
|
||||
is SpaceSelectionMode.Single -> {
|
||||
val spaceName = spaceSelectionMode.spaceRoom?.displayName ?: spaceSelectionMode.spaceId.value
|
||||
stringResource(R.string.screen_security_and_privacy_ask_to_join_single_space_members_option_description, spaceName)
|
||||
}
|
||||
is SpaceSelectionMode.None,
|
||||
is SpaceSelectionMode.Multiple -> stringResource(R.string.screen_security_and_privacy_ask_to_join_multiple_spaces_members_option_description)
|
||||
}
|
||||
} else {
|
||||
stringResource(R.string.screen_security_and_privacy_ask_to_join_option_description)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class SecurityAndPrivacySettings(
|
||||
@@ -76,16 +148,31 @@ enum class SecurityAndPrivacyHistoryVisibility {
|
||||
}
|
||||
}
|
||||
|
||||
enum class SecurityAndPrivacyRoomAccess {
|
||||
InviteOnly,
|
||||
AskToJoin,
|
||||
Anyone,
|
||||
SpaceMember;
|
||||
sealed interface SpaceSelectionMode {
|
||||
data object None : SpaceSelectionMode
|
||||
data class Single(val spaceId: RoomId, val spaceRoom: SpaceRoom?) : SpaceSelectionMode
|
||||
data object Multiple : SpaceSelectionMode
|
||||
}
|
||||
|
||||
sealed interface SecurityAndPrivacyRoomAccess {
|
||||
data object InviteOnly : SecurityAndPrivacyRoomAccess
|
||||
data object AskToJoin : SecurityAndPrivacyRoomAccess
|
||||
data object Anyone : SecurityAndPrivacyRoomAccess
|
||||
data class SpaceMember(val spaceIds: ImmutableList<RoomId>) : SecurityAndPrivacyRoomAccess
|
||||
data class AskToJoinWithSpaceMember(val spaceIds: ImmutableList<RoomId>) : SecurityAndPrivacyRoomAccess
|
||||
|
||||
fun canConfigureRoomVisibility(): Boolean {
|
||||
return when (this) {
|
||||
InviteOnly, SpaceMember -> false
|
||||
AskToJoin, Anyone -> true
|
||||
InviteOnly, is SpaceMember -> false
|
||||
AskToJoin, Anyone, is AskToJoinWithSpaceMember -> true
|
||||
}
|
||||
}
|
||||
|
||||
fun spaceIds(): ImmutableList<RoomId> {
|
||||
return when (this) {
|
||||
is SpaceMember -> spaceIds
|
||||
is AskToJoinWithSpaceMember -> spaceIds
|
||||
else -> persistentListOf()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyPermissions
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
|
||||
open class SecurityAndPrivacyStateProvider : PreviewParameterProvider<SecurityAndPrivacyState> {
|
||||
override val values: Sequence<SecurityAndPrivacyState>
|
||||
@@ -61,11 +64,27 @@ private fun commonSecurityAndPrivacyStates(isSpace: Boolean): Sequence<SecurityA
|
||||
),
|
||||
aSecurityAndPrivacyState(
|
||||
savedSettings = aSecurityAndPrivacySettings(
|
||||
roomAccess = SecurityAndPrivacyRoomAccess.SpaceMember
|
||||
roomAccess = SecurityAndPrivacyRoomAccess.SpaceMember(persistentListOf())
|
||||
),
|
||||
spaceSelectionMode = SpaceSelectionMode.Multiple,
|
||||
isSpace = isSpace,
|
||||
isKnockEnabled = false,
|
||||
),
|
||||
aSecurityAndPrivacyState(
|
||||
spaceSelectionMode = SpaceSelectionMode.Multiple,
|
||||
savedSettings = aSecurityAndPrivacySettings(
|
||||
roomAccess = SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember(persistentListOf()),
|
||||
),
|
||||
isSpace = isSpace,
|
||||
),
|
||||
aSecurityAndPrivacyState(
|
||||
spaceSelectionMode = SpaceSelectionMode.Multiple,
|
||||
savedSettings = aSecurityAndPrivacySettings(
|
||||
roomAccess = SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember(persistentListOf())
|
||||
),
|
||||
isSpace = isSpace,
|
||||
isKnockEnabled = true,
|
||||
),
|
||||
aSecurityAndPrivacyState(
|
||||
editedSettings = aSecurityAndPrivacySettings(
|
||||
roomAccess = SecurityAndPrivacyRoomAccess.Anyone,
|
||||
@@ -117,6 +136,9 @@ fun aSecurityAndPrivacyState(
|
||||
),
|
||||
isKnockEnabled: Boolean = true,
|
||||
isSpace: Boolean = false,
|
||||
selectableJoinedSpaces: Set<SpaceRoom> = emptySet(),
|
||||
spaceSelectionMode: SpaceSelectionMode = SpaceSelectionMode.None,
|
||||
isSpaceSettingsEnabled: Boolean = true,
|
||||
eventSink: (SecurityAndPrivacyEvent) -> Unit = {}
|
||||
) = SecurityAndPrivacyState(
|
||||
editedSettings = editedSettings,
|
||||
@@ -127,5 +149,8 @@ fun aSecurityAndPrivacyState(
|
||||
isKnockEnabled = isKnockEnabled,
|
||||
permissions = permissions,
|
||||
isSpace = isSpace,
|
||||
selectableJoinedSpaces = selectableJoinedSpaces.toImmutableSet(),
|
||||
spaceSelectionMode = spaceSelectionMode,
|
||||
isSpaceSettingsEnabled = isSpaceSettingsEnabled,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
@@ -90,11 +90,8 @@ fun SecurityAndPrivacyView(
|
||||
) {
|
||||
if (state.showRoomAccessSection) {
|
||||
RoomAccessSection(
|
||||
state = state,
|
||||
modifier = Modifier.padding(top = 24.dp),
|
||||
edited = state.editedSettings.roomAccess,
|
||||
saved = state.savedSettings.roomAccess,
|
||||
isKnockEnabled = state.isKnockEnabled,
|
||||
onSelectOption = { state.eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(it)) },
|
||||
)
|
||||
}
|
||||
if (state.showRoomVisibilitySections) {
|
||||
@@ -208,12 +205,27 @@ private fun SecurityAndPrivacySection(
|
||||
|
||||
@Composable
|
||||
private fun RoomAccessSection(
|
||||
edited: SecurityAndPrivacyRoomAccess,
|
||||
saved: SecurityAndPrivacyRoomAccess,
|
||||
isKnockEnabled: Boolean,
|
||||
onSelectOption: (SecurityAndPrivacyRoomAccess) -> Unit,
|
||||
state: SecurityAndPrivacyState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val edited = state.editedSettings.roomAccess
|
||||
|
||||
fun onSelectOption(option: SecurityAndPrivacyRoomAccess) {
|
||||
state.eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(option))
|
||||
}
|
||||
|
||||
fun onSpaceMemberAccessClick() {
|
||||
state.eventSink(SecurityAndPrivacyEvent.SelectSpaceMemberAccess)
|
||||
}
|
||||
|
||||
fun onAskToJoinWithSpaceMembersClick() {
|
||||
state.eventSink(SecurityAndPrivacyEvent.SelectAskToJoinWithSpaceMembersAccess)
|
||||
}
|
||||
|
||||
fun onManageSpacesClick() {
|
||||
state.eventSink(SecurityAndPrivacyEvent.ManageAuthorizedSpaces)
|
||||
}
|
||||
|
||||
SecurityAndPrivacySection(
|
||||
title = stringResource(R.string.screen_security_and_privacy_room_access_section_header),
|
||||
modifier = modifier,
|
||||
@@ -225,29 +237,36 @@ private fun RoomAccessSection(
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Public())),
|
||||
onClick = { onSelectOption(SecurityAndPrivacyRoomAccess.Anyone) },
|
||||
)
|
||||
// Show space member option, but disabled as we don't support this option for now.
|
||||
if (saved == SecurityAndPrivacyRoomAccess.SpaceMember) {
|
||||
if (state.showSpaceMemberOption) {
|
||||
ListItem(
|
||||
headlineContent = { Text(text = stringResource(R.string.screen_security_and_privacy_room_access_space_members_option_title)) },
|
||||
supportingContent = {
|
||||
Text(text = stringResource(R.string.screen_security_and_privacy_room_access_space_members_option_unavailable_description))
|
||||
Text(text = state.spaceMemberDescription())
|
||||
},
|
||||
trailingContent = ListItemContent.RadioButton(selected = edited == SecurityAndPrivacyRoomAccess.SpaceMember, enabled = false),
|
||||
trailingContent = ListItemContent.RadioButton(selected = state.editedSettings.roomAccess is SecurityAndPrivacyRoomAccess.SpaceMember),
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Space())),
|
||||
enabled = false,
|
||||
onClick = ::onSpaceMemberAccessClick,
|
||||
enabled = state.isSpaceMemberSelectable,
|
||||
)
|
||||
}
|
||||
// Show Ask to join option in two cases:
|
||||
// - the Knock FF is enabled
|
||||
// - AskToJoin is the current saved value
|
||||
if (saved == SecurityAndPrivacyRoomAccess.AskToJoin || isKnockEnabled) {
|
||||
if (state.showAskToJoinOption) {
|
||||
ListItem(
|
||||
headlineContent = { Text(text = stringResource(R.string.screen_security_and_privacy_ask_to_join_option_title)) },
|
||||
supportingContent = { Text(text = stringResource(R.string.screen_security_and_privacy_ask_to_join_option_description)) },
|
||||
trailingContent = ListItemContent.RadioButton(selected = edited == SecurityAndPrivacyRoomAccess.AskToJoin),
|
||||
onClick = { onSelectOption(SecurityAndPrivacyRoomAccess.AskToJoin) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.UserAdd())),
|
||||
enabled = isKnockEnabled,
|
||||
enabled = state.isAskToJoinSelectable,
|
||||
)
|
||||
}
|
||||
if (state.showAskToJoinWithSpaceMemberOption) {
|
||||
ListItem(
|
||||
headlineContent = { Text(text = stringResource(R.string.screen_security_and_privacy_ask_to_join_option_title)) },
|
||||
supportingContent = { Text(text = state.askToJoinWithSpaceMembersDescription()) },
|
||||
trailingContent = ListItemContent.RadioButton(selected = edited is SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember),
|
||||
onClick = ::onAskToJoinWithSpaceMembersClick,
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.UserAdd())),
|
||||
enabled = state.isAskToJoinWithSpaceMembersSelectable,
|
||||
)
|
||||
}
|
||||
ListItem(
|
||||
@@ -257,6 +276,20 @@ private fun RoomAccessSection(
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Lock())),
|
||||
onClick = { onSelectOption(SecurityAndPrivacyRoomAccess.InviteOnly) },
|
||||
)
|
||||
if (state.showManageSpaceFooter) {
|
||||
val footerText = stringWithLink(
|
||||
textRes = R.string.screen_security_and_privacy_room_access_footer,
|
||||
url = "",
|
||||
linkTextRes = R.string.screen_security_and_privacy_room_access_footer_manage_spaces_action,
|
||||
onLinkClick = { onManageSpacesClick() },
|
||||
)
|
||||
Text(
|
||||
text = footerText,
|
||||
style = ElementTheme.typography.fontBodySmRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
modifier = Modifier.padding(bottom = 12.dp, start = 56.dp, end = 24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_edit_room_address_room_address_section_footer">"Selleks, et jututuba oleks nähtav jututubade avalikus kataloogis, vajab ta aadressi."</string>
|
||||
<string name="screen_edit_room_address_title">"Muuda aadressi"</string>
|
||||
<string name="screen_manage_authorized_spaces_header">"Kogukonnad, milles on võimalik jututoaga liituda ilma kutseta."</string>
|
||||
<string name="screen_manage_authorized_spaces_title">"Halda kogukondi"</string>
|
||||
<string name="screen_manage_authorized_spaces_unknown_space">"(Tundmatu kogukond)"</string>
|
||||
<string name="screen_manage_authorized_spaces_unknown_spaces_section_title">"Muud kogukonnad, mille liige sa ei ole"</string>
|
||||
<string name="screen_manage_authorized_spaces_your_spaces_section_title">"Sinu kogukonnad"</string>
|
||||
<string name="screen_security_and_privacy_add_room_address_action">"Lisa aadress"</string>
|
||||
<string name="screen_security_and_privacy_ask_to_join_multiple_spaces_members_option_description">"Liituda saavad kõik volitatud kogukondade liikmed, kuid kõik teised peavad küsima võimalust ligipääsuks."</string>
|
||||
<string name="screen_security_and_privacy_ask_to_join_option_description">"Kõik võivad paluda jututoaga liitumist."</string>
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_edit_room_address_room_address_section_footer">"Vous aurez besoin d’une adresse pour le rendre visible dans l’annuaire public."</string>
|
||||
<string name="screen_edit_room_address_title">"Modifier l’adresse"</string>
|
||||
<string name="screen_manage_authorized_spaces_header">"Espaces où les membres peuvent rejoindre le salon sans invitation."</string>
|
||||
<string name="screen_manage_authorized_spaces_title">"Gérer les espaces"</string>
|
||||
<string name="screen_manage_authorized_spaces_unknown_space">"(Espace inconnu)"</string>
|
||||
<string name="screen_manage_authorized_spaces_unknown_spaces_section_title">"Autres espaces dont vous n’êtes pas membre"</string>
|
||||
<string name="screen_manage_authorized_spaces_your_spaces_section_title">"Vos espaces"</string>
|
||||
<string name="screen_security_and_privacy_add_room_address_action">"Ajouter une adresse"</string>
|
||||
<string name="screen_security_and_privacy_ask_to_join_multiple_spaces_members_option_description">"Toute personne se trouvant dans un espace autorisé peut participer, mais toutes les autres doivent demander l’accès."</string>
|
||||
<string name="screen_security_and_privacy_ask_to_join_option_description">"Tout le monde doit demander un accès."</string>
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_edit_room_address_room_address_section_footer">"Trebat će vam adresa kako bi bila vidljiva u javnom direktoriju."</string>
|
||||
<string name="screen_edit_room_address_title">"Uredi adresu"</string>
|
||||
<string name="screen_manage_authorized_spaces_header">"Prostori u kojima se članovi mogu pridružiti sobi bez pozivnice."</string>
|
||||
<string name="screen_manage_authorized_spaces_title">"Upravljaj prostorima"</string>
|
||||
<string name="screen_manage_authorized_spaces_unknown_space">"(nepoznati prostor)"</string>
|
||||
<string name="screen_manage_authorized_spaces_unknown_spaces_section_title">"Drugi prostori čiji niste član"</string>
|
||||
<string name="screen_manage_authorized_spaces_your_spaces_section_title">"Vaši prostori"</string>
|
||||
<string name="screen_security_and_privacy_add_room_address_action">"Dodaj adresu"</string>
|
||||
<string name="screen_security_and_privacy_ask_to_join_multiple_spaces_members_option_description">"Svatko tko se nalazi u ovlaštenim prostorima može se pridružiti, ali svi ostali moraju zatražiti pristup."</string>
|
||||
<string name="screen_security_and_privacy_ask_to_join_option_description">"Svi moraju zatražiti pristup."</string>
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_edit_room_address_room_address_section_footer">"Você precisará de um endereço para torná-la visível no diretório."</string>
|
||||
<string name="screen_edit_room_address_title">"Editar endereço"</string>
|
||||
<string name="screen_manage_authorized_spaces_header">"Os espaços dos quais os membros podem entrar na sala sem um convite."</string>
|
||||
<string name="screen_manage_authorized_spaces_title">"Gerenciar espaços"</string>
|
||||
<string name="screen_manage_authorized_spaces_unknown_space">"(Espaço desconhecido)"</string>
|
||||
<string name="screen_manage_authorized_spaces_unknown_spaces_section_title">"Outros espaços dos quais você não é um membro"</string>
|
||||
<string name="screen_manage_authorized_spaces_your_spaces_section_title">"Seus espaços"</string>
|
||||
<string name="screen_security_and_privacy_add_room_address_action">"Adicionar endereço"</string>
|
||||
<string name="screen_security_and_privacy_ask_to_join_multiple_spaces_members_option_description">"Qualquer um nos espaços autorizados podem entrar, mas todos os outros devem pedir acesso."</string>
|
||||
<string name="screen_security_and_privacy_ask_to_join_option_description">"Qualquer um pode pedir acesso, mas um administrador terá que aceitar o pedido."</string>
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_edit_room_address_room_address_section_footer">"Veți avea nevoie de o adresă pentru a o face vizibilă în directorul public."</string>
|
||||
<string name="screen_edit_room_address_title">"Editați adresa"</string>
|
||||
<string name="screen_manage_authorized_spaces_header">"Spațile din care membrii se pot alătura camerei fără invitație."</string>
|
||||
<string name="screen_manage_authorized_spaces_title">"Gestionați spațiile"</string>
|
||||
<string name="screen_manage_authorized_spaces_unknown_space">"(Spațiu necunoscut)"</string>
|
||||
<string name="screen_manage_authorized_spaces_unknown_spaces_section_title">"Alte spații din care nu faceți parte"</string>
|
||||
<string name="screen_manage_authorized_spaces_your_spaces_section_title">"Spațiile dumneavoastră"</string>
|
||||
<string name="screen_security_and_privacy_add_room_address_action">"Adăugați o adresă"</string>
|
||||
<string name="screen_security_and_privacy_ask_to_join_multiple_spaces_members_option_description">"Oricine se află în spațiile autorizate se poate alătura, dar toți ceilalți trebuie să solicite accesul."</string>
|
||||
<string name="screen_security_and_privacy_ask_to_join_option_description">"Toată lumea trebuie să solicite acces."</string>
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_edit_room_address_room_address_section_footer">"Budete potrebovať adresu, aby sa zobrazovala vo verejnom adresári."</string>
|
||||
<string name="screen_edit_room_address_title">"Upraviť adresu"</string>
|
||||
<string name="screen_manage_authorized_spaces_header">"Priestory, kde sa členovia môžu pripojiť k miestnosti bez pozvania."</string>
|
||||
<string name="screen_manage_authorized_spaces_title">"Spravovať priestory"</string>
|
||||
<string name="screen_manage_authorized_spaces_unknown_space">"(Neznámy priestor)"</string>
|
||||
<string name="screen_manage_authorized_spaces_unknown_spaces_section_title">"Iné priestory, ktorých nie ste členom"</string>
|
||||
<string name="screen_manage_authorized_spaces_your_spaces_section_title">"Vaše priestory"</string>
|
||||
<string name="screen_security_and_privacy_add_room_address_action">"Pridať adresu"</string>
|
||||
<string name="screen_security_and_privacy_ask_to_join_option_description">"Všetci musia požiadať o prístup."</string>
|
||||
<string name="screen_security_and_privacy_ask_to_join_option_title">"Požiadať o pripojenie"</string>
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_edit_room_address_room_address_section_footer">"You’ll need an address in order to make it visible in the public directory."</string>
|
||||
<string name="screen_edit_room_address_title">"Edit address"</string>
|
||||
<string name="screen_manage_authorized_spaces_header">"Spaces where members can join the room without an invitation."</string>
|
||||
<string name="screen_manage_authorized_spaces_title">"Manage spaces"</string>
|
||||
<string name="screen_manage_authorized_spaces_unknown_space">"(Unknown space)"</string>
|
||||
<string name="screen_manage_authorized_spaces_unknown_spaces_section_title">"Other spaces you’re not a member of"</string>
|
||||
<string name="screen_manage_authorized_spaces_your_spaces_section_title">"Your spaces"</string>
|
||||
<string name="screen_security_and_privacy_add_room_address_action">"Add address"</string>
|
||||
<string name="screen_security_and_privacy_ask_to_join_multiple_spaces_members_option_description">"Anyone in authorised spaces can join, but everyone else must request access."</string>
|
||||
<string name="screen_security_and_privacy_ask_to_join_option_description">"Everyone must request access."</string>
|
||||
|
||||
@@ -14,6 +14,8 @@ class FakeSecurityAndPrivacyNavigator(
|
||||
private val onDoneLambda: () -> Unit = { lambdaError() },
|
||||
private val openEditRoomAddressLambda: () -> Unit = { lambdaError() },
|
||||
private val closeEditRoomAddressLambda: () -> Unit = { lambdaError() },
|
||||
private val openManageAuthorizedSpacesLambda: () -> Unit = { lambdaError() },
|
||||
private val closeManageAuthorizedSpacesLambda: () -> Unit = { lambdaError() },
|
||||
) : SecurityAndPrivacyNavigator {
|
||||
override fun onDone() {
|
||||
onDoneLambda()
|
||||
@@ -26,4 +28,12 @@ class FakeSecurityAndPrivacyNavigator(
|
||||
override fun closeEditRoomAddress() {
|
||||
closeEditRoomAddressLambda()
|
||||
}
|
||||
|
||||
override fun openManageAuthorizedSpaces() {
|
||||
openManageAuthorizedSpacesLambda()
|
||||
}
|
||||
|
||||
override fun closeManageAuthorizedSpaces() {
|
||||
closeManageAuthorizedSpacesLambda()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.securityandprivacy.impl
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.bumble.appyx.core.modality.AncestryInfo
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.navmodel.backstack.activeElement
|
||||
import com.bumble.appyx.utils.customisations.NodeCustomisationDirectoryImpl
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyEntryPoint
|
||||
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SecurityAndPrivacyFlowNodeTest {
|
||||
@Test
|
||||
fun `initial backstack contains SecurityAndPrivacy`() = runTest {
|
||||
val flowNode = createFlowNode()
|
||||
assertThat(flowNode.currentNavTarget()).isEqualTo(SecurityAndPrivacyFlowNode.NavTarget.SecurityAndPrivacy)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `openEditRoomAddress navigates to EditRoomAddress`() = runTest {
|
||||
val flowNode = createFlowNode()
|
||||
flowNode.navigator.openEditRoomAddress()
|
||||
assertThat(flowNode.currentNavTarget()).isEqualTo(SecurityAndPrivacyFlowNode.NavTarget.EditRoomAddress)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `closeEditRoomAddress pops backstack`() = runTest {
|
||||
val flowNode = createFlowNode()
|
||||
flowNode.navigator.openEditRoomAddress()
|
||||
assertThat(flowNode.currentNavTarget()).isEqualTo(SecurityAndPrivacyFlowNode.NavTarget.EditRoomAddress)
|
||||
flowNode.navigator.closeEditRoomAddress()
|
||||
assertThat(flowNode.currentNavTarget()).isEqualTo(SecurityAndPrivacyFlowNode.NavTarget.SecurityAndPrivacy)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `openManageAuthorizedSpaces navigates to ManageAuthorizedSpaces`() = runTest {
|
||||
val flowNode = createFlowNode()
|
||||
flowNode.navigator.openManageAuthorizedSpaces()
|
||||
assertThat(flowNode.currentNavTarget()).isEqualTo(SecurityAndPrivacyFlowNode.NavTarget.ManageAuthorizedSpaces)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `closeManageAuthorizedSpaces pops backstack`() = runTest {
|
||||
val flowNode = createFlowNode()
|
||||
flowNode.navigator.openManageAuthorizedSpaces()
|
||||
assertThat(flowNode.currentNavTarget())
|
||||
.isInstanceOf(SecurityAndPrivacyFlowNode.NavTarget.ManageAuthorizedSpaces::class.java)
|
||||
flowNode.navigator.closeManageAuthorizedSpaces()
|
||||
assertThat(flowNode.currentNavTarget()).isEqualTo(SecurityAndPrivacyFlowNode.NavTarget.SecurityAndPrivacy)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onDone invokes callback`() = runTest {
|
||||
var onDoneCalled = false
|
||||
val callback = object : SecurityAndPrivacyEntryPoint.Callback {
|
||||
override fun onDone() {
|
||||
onDoneCalled = true
|
||||
}
|
||||
}
|
||||
val flowNode = createFlowNode(callback = callback)
|
||||
flowNode.navigator.onDone()
|
||||
assertThat(onDoneCalled).isTrue()
|
||||
}
|
||||
|
||||
private fun createFlowNode(
|
||||
callback: SecurityAndPrivacyEntryPoint.Callback = object : SecurityAndPrivacyEntryPoint.Callback {
|
||||
override fun onDone() {}
|
||||
},
|
||||
): SecurityAndPrivacyFlowNode {
|
||||
val buildContext = BuildContext(
|
||||
ancestryInfo = AncestryInfo.Root,
|
||||
savedStateMap = null,
|
||||
customisations = NodeCustomisationDirectoryImpl()
|
||||
)
|
||||
val room = FakeJoinedRoom(
|
||||
baseRoom = FakeBaseRoom(
|
||||
initialRoomInfo = aRoomInfo(
|
||||
joinRule = JoinRule.Invite,
|
||||
historyVisibility = RoomHistoryVisibility.Shared
|
||||
)
|
||||
)
|
||||
)
|
||||
return SecurityAndPrivacyFlowNode(
|
||||
buildContext = buildContext,
|
||||
plugins = listOf(callback),
|
||||
room = room,
|
||||
)
|
||||
}
|
||||
|
||||
private fun SecurityAndPrivacyFlowNode.currentNavTarget() = backstack.activeElement
|
||||
}
|
||||
@@ -1,421 +0,0 @@
|
||||
/*
|
||||
* 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.securityandprivacy.impl
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyEvent
|
||||
import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyHistoryVisibility
|
||||
import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyPresenter
|
||||
import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyRoomAccess
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.api.room.StateEventType
|
||||
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPermissions
|
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ALIAS
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||
import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions
|
||||
import io.element.android.tests.testutils.lambda.assert
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.test
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class SecurityAndPrivacyPresenterTest {
|
||||
@Test
|
||||
fun `present - initial states`() = runTest {
|
||||
val presenter = createSecurityAndPrivacyPresenter()
|
||||
presenter.test {
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings).isEqualTo(savedSettings)
|
||||
assertThat(canBeSaved).isFalse()
|
||||
assertThat(showEnableEncryptionConfirmation).isFalse()
|
||||
assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
assertThat(showRoomAccessSection).isFalse()
|
||||
assertThat(showRoomVisibilitySections).isFalse()
|
||||
assertThat(showHistoryVisibilitySection).isFalse()
|
||||
assertThat(showEncryptionSection).isFalse()
|
||||
assertThat(isKnockEnabled).isFalse()
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings).isEqualTo(savedSettings)
|
||||
assertThat(canBeSaved).isFalse()
|
||||
assertThat(showEnableEncryptionConfirmation).isFalse()
|
||||
assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
assertThat(showRoomAccessSection).isTrue()
|
||||
assertThat(showRoomVisibilitySections).isFalse()
|
||||
assertThat(showHistoryVisibilitySection).isTrue()
|
||||
assertThat(showEncryptionSection).isTrue()
|
||||
assertThat(isKnockEnabled).isFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - room info change updates saved and edited settings`() = runTest {
|
||||
val room = FakeJoinedRoom(
|
||||
baseRoom = FakeBaseRoom(
|
||||
roomPermissions = roomPermissions(),
|
||||
initialRoomInfo = aRoomInfo(
|
||||
joinRule = JoinRule.Public,
|
||||
historyVisibility = RoomHistoryVisibility.WorldReadable,
|
||||
canonicalAlias = A_ROOM_ALIAS,
|
||||
)
|
||||
)
|
||||
)
|
||||
val presenter = createSecurityAndPrivacyPresenter(room = room)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings).isEqualTo(savedSettings)
|
||||
assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.Anyone)
|
||||
assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.WorldReadable)
|
||||
assertThat(editedSettings.address).isEqualTo(A_ROOM_ALIAS.value)
|
||||
assertThat(canBeSaved).isFalse()
|
||||
}
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - change room access`() = runTest {
|
||||
val presenter = createSecurityAndPrivacyPresenter()
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.InviteOnly)
|
||||
assertThat(showRoomVisibilitySections).isFalse()
|
||||
eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.Anyone))
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.Anyone)
|
||||
assertThat(showRoomVisibilitySections).isTrue()
|
||||
assertThat(canBeSaved).isTrue()
|
||||
eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.InviteOnly))
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.InviteOnly)
|
||||
assertThat(showRoomVisibilitySections).isFalse()
|
||||
assertThat(canBeSaved).isFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - change history visibility`() = runTest {
|
||||
val presenter = createSecurityAndPrivacyPresenter()
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.Shared)
|
||||
eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.Invited))
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.Invited)
|
||||
assertThat(canBeSaved).isTrue()
|
||||
eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.Shared))
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.Shared)
|
||||
assertThat(canBeSaved).isFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - enable encryption`() = runTest {
|
||||
val presenter = createSecurityAndPrivacyPresenter()
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.isEncrypted).isFalse()
|
||||
eventSink(SecurityAndPrivacyEvent.ToggleEncryptionState)
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(showEnableEncryptionConfirmation).isTrue()
|
||||
eventSink(SecurityAndPrivacyEvent.CancelEnableEncryption)
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(showEnableEncryptionConfirmation).isFalse()
|
||||
eventSink(SecurityAndPrivacyEvent.ToggleEncryptionState)
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(showEnableEncryptionConfirmation).isTrue()
|
||||
eventSink(SecurityAndPrivacyEvent.ConfirmEnableEncryption)
|
||||
}
|
||||
skipItems(1)
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.isEncrypted).isTrue()
|
||||
assertThat(showEnableEncryptionConfirmation).isFalse()
|
||||
assertThat(canBeSaved).isTrue()
|
||||
eventSink(SecurityAndPrivacyEvent.ToggleEncryptionState)
|
||||
}
|
||||
skipItems(1)
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.isEncrypted).isFalse()
|
||||
assertThat(canBeSaved).isFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - room visibility loading and change`() = runTest {
|
||||
val room = FakeJoinedRoom(
|
||||
baseRoom = FakeBaseRoom(
|
||||
roomPermissions = roomPermissions(),
|
||||
getRoomVisibilityResult = { Result.success(RoomVisibility.Private) },
|
||||
initialRoomInfo = aRoomInfo(historyVisibility = RoomHistoryVisibility.Shared)
|
||||
)
|
||||
)
|
||||
val presenter = createSecurityAndPrivacyPresenter(room = room)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Loading<Boolean>())
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(false))
|
||||
eventSink(SecurityAndPrivacyEvent.ToggleRoomVisibility)
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(true))
|
||||
assertThat(canBeSaved).isTrue()
|
||||
eventSink(SecurityAndPrivacyEvent.ToggleRoomVisibility)
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(false))
|
||||
assertThat(canBeSaved).isFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - edit room address`() = runTest {
|
||||
val openEditRoomAddressLambda = lambdaRecorder<Unit> { }
|
||||
val navigator = FakeSecurityAndPrivacyNavigator(openEditRoomAddressLambda = openEditRoomAddressLambda)
|
||||
val presenter = createSecurityAndPrivacyPresenter(navigator = navigator)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
with(awaitItem()) {
|
||||
eventSink(SecurityAndPrivacyEvent.EditRoomAddress)
|
||||
}
|
||||
assert(openEditRoomAddressLambda).isCalledOnce()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - save success`() = runTest {
|
||||
val enableEncryptionLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
|
||||
val updateJoinRuleLambda = lambdaRecorder<JoinRule, Result<Unit>> { Result.success(Unit) }
|
||||
val updateRoomVisibilityLambda = lambdaRecorder<RoomVisibility, Result<Unit>> { Result.success(Unit) }
|
||||
val updateRoomHistoryVisibilityLambda = lambdaRecorder<RoomHistoryVisibility, Result<Unit>> { Result.success(Unit) }
|
||||
val room = FakeJoinedRoom(
|
||||
baseRoom = FakeBaseRoom(
|
||||
roomPermissions = roomPermissions(),
|
||||
getRoomVisibilityResult = { Result.success(RoomVisibility.Private) },
|
||||
initialRoomInfo = aRoomInfo(joinRule = JoinRule.Invite, historyVisibility = RoomHistoryVisibility.Shared)
|
||||
),
|
||||
enableEncryptionResult = enableEncryptionLambda,
|
||||
updateJoinRuleResult = updateJoinRuleLambda,
|
||||
updateRoomVisibilityResult = updateRoomVisibilityLambda,
|
||||
updateRoomHistoryVisibilityResult = updateRoomHistoryVisibilityLambda,
|
||||
)
|
||||
val onDoneLambda = lambdaRecorder<Unit> { }
|
||||
val navigator = FakeSecurityAndPrivacyNavigator(
|
||||
onDoneLambda = onDoneLambda,
|
||||
)
|
||||
val presenter = createSecurityAndPrivacyPresenter(
|
||||
room = room,
|
||||
navigator = navigator,
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.InviteOnly)
|
||||
eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.Anyone))
|
||||
}
|
||||
with(awaitItem()) {
|
||||
eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.WorldReadable))
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.WorldReadable)
|
||||
eventSink(SecurityAndPrivacyEvent.ConfirmEnableEncryption)
|
||||
}
|
||||
skipItems(1)
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.isEncrypted).isTrue()
|
||||
eventSink(SecurityAndPrivacyEvent.ToggleRoomVisibility)
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(true))
|
||||
eventSink(SecurityAndPrivacyEvent.Save)
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(saveAction).isEqualTo(AsyncAction.Loading)
|
||||
}
|
||||
|
||||
room.givenRoomInfo(
|
||||
aRoomInfo(
|
||||
joinRule = JoinRule.Public,
|
||||
historyVisibility = RoomHistoryVisibility.WorldReadable,
|
||||
isEncrypted = true,
|
||||
)
|
||||
)
|
||||
// Saved settings are updated 2 times to match the edited settings
|
||||
skipItems(2)
|
||||
with(awaitItem()) {
|
||||
assertThat(saveAction).isEqualTo(AsyncAction.Success(Unit))
|
||||
assertThat(savedSettings).isEqualTo(editedSettings)
|
||||
assertThat(canBeSaved).isFalse()
|
||||
}
|
||||
assert(enableEncryptionLambda).isCalledOnce()
|
||||
assert(updateJoinRuleLambda).isCalledOnce()
|
||||
assert(updateRoomVisibilityLambda).isCalledOnce()
|
||||
assert(updateRoomHistoryVisibilityLambda).isCalledOnce()
|
||||
onDoneLambda.assertions().isCalledOnce()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - save failure`() = runTest {
|
||||
val enableEncryptionLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
|
||||
val updateJoinRuleLambda = lambdaRecorder<JoinRule, Result<Unit>> { Result.success(Unit) }
|
||||
val updateRoomVisibilityLambda = lambdaRecorder<RoomVisibility, Result<Unit>> {
|
||||
Result.failure(Exception("Failed to update room visibility"))
|
||||
}
|
||||
val updateRoomHistoryVisibilityLambda = lambdaRecorder<RoomHistoryVisibility, Result<Unit>> { Result.success(Unit) }
|
||||
val room = FakeJoinedRoom(
|
||||
baseRoom = FakeBaseRoom(
|
||||
roomPermissions = roomPermissions(),
|
||||
getRoomVisibilityResult = { Result.success(RoomVisibility.Private) },
|
||||
initialRoomInfo = aRoomInfo(historyVisibility = RoomHistoryVisibility.Shared, joinRule = JoinRule.Private)
|
||||
),
|
||||
enableEncryptionResult = enableEncryptionLambda,
|
||||
updateJoinRuleResult = updateJoinRuleLambda,
|
||||
updateRoomVisibilityResult = updateRoomVisibilityLambda,
|
||||
updateRoomHistoryVisibilityResult = updateRoomHistoryVisibilityLambda,
|
||||
)
|
||||
val presenter = createSecurityAndPrivacyPresenter(room = room)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.InviteOnly)
|
||||
eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.Anyone))
|
||||
}
|
||||
with(awaitItem()) {
|
||||
eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.WorldReadable))
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.WorldReadable)
|
||||
eventSink(SecurityAndPrivacyEvent.ConfirmEnableEncryption)
|
||||
}
|
||||
skipItems(1)
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.isEncrypted).isTrue()
|
||||
eventSink(SecurityAndPrivacyEvent.ToggleRoomVisibility)
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(true))
|
||||
eventSink(SecurityAndPrivacyEvent.Save)
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(saveAction).isEqualTo(AsyncAction.Loading)
|
||||
}
|
||||
|
||||
room.givenRoomInfo(
|
||||
aRoomInfo(
|
||||
joinRule = JoinRule.Public,
|
||||
historyVisibility = RoomHistoryVisibility.WorldReadable,
|
||||
)
|
||||
)
|
||||
// Saved settings are updated 2 times to match the edited settings
|
||||
skipItems(2)
|
||||
val state = awaitItem()
|
||||
with(state) {
|
||||
assertThat(saveAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||
assertThat(savedSettings.isVisibleInRoomDirectory).isNotEqualTo(editedSettings.isVisibleInRoomDirectory)
|
||||
assertThat(canBeSaved).isTrue()
|
||||
}
|
||||
assert(enableEncryptionLambda).isCalledOnce()
|
||||
assert(updateJoinRuleLambda).isCalledOnce()
|
||||
assert(updateRoomVisibilityLambda).isCalledOnce()
|
||||
assert(updateRoomHistoryVisibilityLambda).isCalledOnce()
|
||||
// Clear error
|
||||
state.eventSink(SecurityAndPrivacyEvent.DismissSaveError)
|
||||
with(awaitItem()) {
|
||||
assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - isKnockEnabled is true if the Knock feature flag is enabled`() = runTest {
|
||||
val presenter = createSecurityAndPrivacyPresenter(
|
||||
featureFlagService = FakeFeatureFlagService(
|
||||
initialState = mapOf(
|
||||
FeatureFlags.Knock.key to true,
|
||||
)
|
||||
)
|
||||
)
|
||||
presenter.test {
|
||||
assertThat(awaitItem().isKnockEnabled).isFalse()
|
||||
assertThat(awaitItem().isKnockEnabled).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
private fun roomPermissions(
|
||||
canChangeRoomAccess: Boolean = true,
|
||||
canChangeHistoryVisibility: Boolean = true,
|
||||
canChangeEncryption: Boolean = true,
|
||||
canChangeRoomVisibility: Boolean = true,
|
||||
): RoomPermissions {
|
||||
return FakeRoomPermissions(
|
||||
canSendState = { eventType ->
|
||||
when (eventType) {
|
||||
StateEventType.RoomJoinRules -> canChangeRoomAccess
|
||||
StateEventType.RoomHistoryVisibility -> canChangeHistoryVisibility
|
||||
StateEventType.RoomEncryption -> canChangeEncryption
|
||||
StateEventType.RoomCanonicalAlias -> canChangeRoomVisibility
|
||||
else -> lambdaError()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun createSecurityAndPrivacyPresenter(
|
||||
serverName: String = "matrix.org",
|
||||
room: FakeJoinedRoom = FakeJoinedRoom(
|
||||
baseRoom = FakeBaseRoom(
|
||||
roomPermissions = roomPermissions(),
|
||||
getRoomVisibilityResult = { Result.success(RoomVisibility.Private) },
|
||||
initialRoomInfo = aRoomInfo(historyVisibility = RoomHistoryVisibility.Shared, joinRule = JoinRule.Private)
|
||||
),
|
||||
),
|
||||
navigator: SecurityAndPrivacyNavigator = FakeSecurityAndPrivacyNavigator(),
|
||||
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
||||
): SecurityAndPrivacyPresenter {
|
||||
return SecurityAndPrivacyPresenter(
|
||||
room = room,
|
||||
matrixClient = FakeMatrixClient(
|
||||
userIdServerNameLambda = { serverName },
|
||||
),
|
||||
navigator = navigator,
|
||||
featureFlagService = featureFlagService,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* 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.securityandprivacy.impl.manageauthorizedspaces
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.tests.testutils.test
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class ManageAuthorizedSpacesPresenterTest {
|
||||
@Test
|
||||
fun `present - initial state reflects shared state`() = runTest {
|
||||
val sharedStateHolder = SpaceSelectionStateHolder()
|
||||
val presenter = ManageAuthorizedSpacesPresenter(sharedStateHolder)
|
||||
presenter.test {
|
||||
with(awaitItem()) {
|
||||
assertThat(selectedIds).isEmpty()
|
||||
assertThat(isDoneButtonEnabled).isFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - state reflects shared state with pre-selected spaces`() = runTest {
|
||||
val sharedStateHolder = SpaceSelectionStateHolder()
|
||||
val roomId = A_ROOM_ID
|
||||
sharedStateHolder.update {
|
||||
it.copy(selectedSpaceIds = persistentListOf(roomId))
|
||||
}
|
||||
val presenter = ManageAuthorizedSpacesPresenter(sharedStateHolder)
|
||||
presenter.test {
|
||||
with(awaitItem()) {
|
||||
assertThat(selectedIds).containsExactly(roomId)
|
||||
assertThat(isDoneButtonEnabled).isTrue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - ToggleSpace event adds space to selectedIds in shared state`() = runTest {
|
||||
val sharedStateHolder = SpaceSelectionStateHolder()
|
||||
val presenter = ManageAuthorizedSpacesPresenter(sharedStateHolder)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
val roomId = A_ROOM_ID
|
||||
initialState.eventSink(ManageAuthorizedSpacesEvent.ToggleSpace(roomId))
|
||||
with(awaitItem()) {
|
||||
assertThat(selectedIds).containsExactly(roomId)
|
||||
assertThat(isDoneButtonEnabled).isTrue()
|
||||
}
|
||||
// Verify the shared state is also updated
|
||||
assertThat(sharedStateHolder.state.value.selectedSpaceIds).containsExactly(roomId)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - ToggleSpace event removes space when already selected`() = runTest {
|
||||
val sharedStateHolder = SpaceSelectionStateHolder()
|
||||
sharedStateHolder.updateSelectedSpaceIds(persistentListOf(A_ROOM_ID))
|
||||
val presenter = ManageAuthorizedSpacesPresenter(sharedStateHolder)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.selectedIds).containsExactly(A_ROOM_ID)
|
||||
initialState.eventSink(ManageAuthorizedSpacesEvent.ToggleSpace(A_ROOM_ID))
|
||||
with(awaitItem()) {
|
||||
assertThat(selectedIds).isEmpty()
|
||||
assertThat(isDoneButtonEnabled).isFalse()
|
||||
}
|
||||
// Verify the shared state is also updated
|
||||
assertThat(sharedStateHolder.state.value.selectedSpaceIds).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - Done event sets completion to Completed`() = runTest {
|
||||
val sharedStateHolder = SpaceSelectionStateHolder()
|
||||
val presenter = ManageAuthorizedSpacesPresenter(sharedStateHolder)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(ManageAuthorizedSpacesEvent.Done)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
assertThat(sharedStateHolder.state.value.completion)
|
||||
.isEqualTo(SpaceSelectionState.Completion.Completed)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - Cancel event sets completion to Cancelled`() = runTest {
|
||||
val sharedStateHolder = SpaceSelectionStateHolder()
|
||||
val presenter = ManageAuthorizedSpacesPresenter(sharedStateHolder)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(ManageAuthorizedSpacesEvent.Cancel)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
assertThat(sharedStateHolder.state.value.completion)
|
||||
.isEqualTo(SpaceSelectionState.Completion.Cancelled)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - displays spaces from shared state`() = runTest {
|
||||
val sharedStateHolder = SpaceSelectionStateHolder()
|
||||
sharedStateHolder.update {
|
||||
it.copy(
|
||||
selectableSpaces = persistentSetOf(),
|
||||
unknownSpaceIds = persistentListOf(A_ROOM_ID),
|
||||
)
|
||||
}
|
||||
val presenter = ManageAuthorizedSpacesPresenter(sharedStateHolder)
|
||||
presenter.test {
|
||||
with(awaitItem()) {
|
||||
assertThat(selectableSpaces).isEmpty()
|
||||
assertThat(unknownSpaceIds).containsExactly(A_ROOM_ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* 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.securityandprivacy.impl.manageauthorizedspaces
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
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.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.EventsRecorder
|
||||
import io.element.android.tests.testutils.clickOn
|
||||
import io.element.android.tests.testutils.pressBack
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ManageAuthorizedSpacesViewTest {
|
||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `clicking back emits Cancel event`() {
|
||||
val recorder = EventsRecorder<ManageAuthorizedSpacesEvent>()
|
||||
val state = aManageAuthorizedSpacesState(eventSink = recorder)
|
||||
rule.setManageAuthorizedSpacesView(state)
|
||||
rule.pressBack()
|
||||
recorder.assertSingle(ManageAuthorizedSpacesEvent.Cancel)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking space checkbox emits ToggleSpace event`() {
|
||||
val roomId = A_ROOM_ID
|
||||
val space = aSpaceRoom(roomId = roomId, displayName = "Test Space")
|
||||
val recorder = EventsRecorder<ManageAuthorizedSpacesEvent>()
|
||||
val state = aManageAuthorizedSpacesState(
|
||||
selectableSpaces = listOf(space),
|
||||
eventSink = recorder
|
||||
)
|
||||
rule.setManageAuthorizedSpacesView(state)
|
||||
rule.onNodeWithText("Test Space").performClick()
|
||||
recorder.assertSingle(ManageAuthorizedSpacesEvent.ToggleSpace(roomId))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking done button emits Done event`() {
|
||||
val recorder = EventsRecorder<ManageAuthorizedSpacesEvent>()
|
||||
val state = aManageAuthorizedSpacesState(
|
||||
selectedIds = listOf(A_ROOM_ID),
|
||||
eventSink = recorder
|
||||
)
|
||||
rule.setManageAuthorizedSpacesView(state)
|
||||
rule.clickOn(CommonStrings.action_done)
|
||||
recorder.assertSingle(ManageAuthorizedSpacesEvent.Done)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `done button is disabled when no spaces selected`() {
|
||||
val recorder = EventsRecorder<ManageAuthorizedSpacesEvent>(expectEvents = false)
|
||||
val state = aManageAuthorizedSpacesState(
|
||||
selectedIds = emptyList(),
|
||||
eventSink = recorder
|
||||
)
|
||||
rule.setManageAuthorizedSpacesView(state)
|
||||
rule.clickOn(CommonStrings.action_done)
|
||||
recorder.assertEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setManageAuthorizedSpacesView(
|
||||
state: ManageAuthorizedSpacesState = aManageAuthorizedSpacesState(
|
||||
eventSink = EventsRecorder(expectEvents = false)
|
||||
),
|
||||
) {
|
||||
setContent {
|
||||
ManageAuthorizedSpacesView(state = state)
|
||||
}
|
||||
}
|
||||
|
||||
private fun aManageAuthorizedSpacesState(
|
||||
selectableSpaces: List<SpaceRoom> = emptyList(),
|
||||
unknownSpaceIds: List<RoomId> = emptyList(),
|
||||
selectedIds: List<RoomId> = emptyList(),
|
||||
eventSink: (ManageAuthorizedSpacesEvent) -> Unit = {},
|
||||
) = ManageAuthorizedSpacesState(
|
||||
selectableSpaces = selectableSpaces.toImmutableSet(),
|
||||
unknownSpaceIds = unknownSpaceIds.toImmutableList(),
|
||||
selectedIds = selectedIds.toImmutableList(),
|
||||
eventSink = eventSink,
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,11 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
* Copyright (c) 2026 Element Creations 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.securityandprivacy.impl
|
||||
package io.element.android.features.securityandprivacy.impl.root
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
@@ -14,20 +13,16 @@ 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.features.securityandprivacy.impl.root.SecurityAndPrivacyEvent
|
||||
import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyHistoryVisibility
|
||||
import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyRoomAccess
|
||||
import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyState
|
||||
import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyView
|
||||
import io.element.android.features.securityandprivacy.impl.root.aSecurityAndPrivacySettings
|
||||
import io.element.android.features.securityandprivacy.impl.root.aSecurityAndPrivacyState
|
||||
import io.element.android.features.securityandprivacy.impl.R
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
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.pressBack
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TestRule
|
||||
@@ -179,6 +174,50 @@ class SecurityAndPrivacyViewTest {
|
||||
rule.clickOn(R.string.screen_security_and_privacy_enable_encryption_alert_confirm_button_title)
|
||||
recorder.assertSingle(SecurityAndPrivacyEvent.ConfirmEnableEncryption)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(qualifiers = "h1024dp")
|
||||
fun `click on space member access emits the expected event`() {
|
||||
val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
|
||||
val state = aSecurityAndPrivacyState(
|
||||
eventSink = recorder,
|
||||
spaceSelectionMode = SpaceSelectionMode.Single(A_ROOM_ID, null),
|
||||
)
|
||||
rule.setSecurityAndPrivacyView(state)
|
||||
rule.clickOn(R.string.screen_security_and_privacy_room_access_space_members_option_title)
|
||||
recorder.assertSingle(SecurityAndPrivacyEvent.SelectSpaceMemberAccess)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(qualifiers = "h1024dp")
|
||||
fun `click on ask to join with space members emits the expected event`() {
|
||||
val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
|
||||
val state = aSecurityAndPrivacyState(
|
||||
eventSink = recorder,
|
||||
spaceSelectionMode = SpaceSelectionMode.Single(A_ROOM_ID, null),
|
||||
)
|
||||
rule.setSecurityAndPrivacyView(state)
|
||||
rule.clickOn(R.string.screen_security_and_privacy_ask_to_join_option_title)
|
||||
recorder.assertSingle(SecurityAndPrivacyEvent.SelectAskToJoinWithSpaceMembersAccess)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(qualifiers = "h1024dp")
|
||||
fun `manage spaces footer is shown when space member access is selected`() {
|
||||
val recorder = EventsRecorder<SecurityAndPrivacyEvent>(expectEvents = false)
|
||||
val state = aSecurityAndPrivacyState(
|
||||
eventSink = recorder,
|
||||
spaceSelectionMode = SpaceSelectionMode.Multiple,
|
||||
editedSettings = aSecurityAndPrivacySettings(
|
||||
roomAccess = SecurityAndPrivacyRoomAccess.SpaceMember(persistentListOf(A_ROOM_ID)),
|
||||
),
|
||||
)
|
||||
rule.setSecurityAndPrivacyView(state)
|
||||
// The footer text uses AnnotatedString with a link. Verify the footer text is displayed.
|
||||
val actionFooterText = rule.activity.getString(R.string.screen_security_and_privacy_room_access_footer_manage_spaces_action)
|
||||
val footerText = rule.activity.getString(R.string.screen_security_and_privacy_room_access_footer, actionFooterText)
|
||||
rule.onNodeWithText(footerText).assertExists()
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSecurityAndPrivacyView(
|
||||
@@ -15,6 +15,10 @@ interface SpaceService {
|
||||
val spaceRoomsFlow: SharedFlow<List<SpaceRoom>>
|
||||
suspend fun joinedSpaces(): Result<List<SpaceRoom>>
|
||||
|
||||
suspend fun joinedParents(spaceId: RoomId): Result<List<SpaceRoom>>
|
||||
|
||||
suspend fun getSpaceRoom(spaceId: RoomId): SpaceRoom?
|
||||
|
||||
fun spaceRoomList(id: RoomId): SpaceRoomList
|
||||
|
||||
fun getLeaveSpaceHandle(spaceId: RoomId): LeaveSpaceHandle
|
||||
|
||||
@@ -51,13 +51,28 @@ class RustSpaceService(
|
||||
|
||||
override suspend fun joinedSpaces(): Result<List<SpaceRoom>> = withContext(sessionDispatcher) {
|
||||
runCatchingExceptions {
|
||||
innerSpaceService.topLevelJoinedSpaces()
|
||||
.map {
|
||||
it.let(spaceRoomMapper::map)
|
||||
}
|
||||
innerSpaceService
|
||||
.topLevelJoinedSpaces()
|
||||
.map(spaceRoomMapper::map)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun joinedParents(spaceId: RoomId): Result<List<SpaceRoom>> = withContext(sessionDispatcher) {
|
||||
runCatchingExceptions {
|
||||
innerSpaceService
|
||||
.joinedParentsOfChild(spaceId.value)
|
||||
.map(spaceRoomMapper::map)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getSpaceRoom(spaceId: RoomId): SpaceRoom? = withContext(sessionDispatcher) {
|
||||
runCatchingExceptions {
|
||||
innerSpaceService.getSpaceRoom(spaceId.value)?.let { spaceRoom ->
|
||||
spaceRoomMapper.map(spaceRoom)
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
override fun spaceRoomList(id: RoomId): SpaceRoomList {
|
||||
val childCoroutineScope = sessionCoroutineScope.childScope(sessionDispatcher, "SpaceRoomListScope-$this")
|
||||
return RustSpaceRoomList(
|
||||
|
||||
@@ -23,6 +23,8 @@ class FakeSpaceService(
|
||||
private val joinedSpacesResult: () -> Result<List<SpaceRoom>> = { lambdaError() },
|
||||
private val spaceRoomListResult: (RoomId) -> SpaceRoomList = { lambdaError() },
|
||||
private val leaveSpaceHandleResult: (RoomId) -> LeaveSpaceHandle = { lambdaError() },
|
||||
private val joinedParentsResult: (RoomId) -> Result<List<SpaceRoom>> = { lambdaError() },
|
||||
private val getSpaceRoomResult: (RoomId) -> SpaceRoom? = { lambdaError() },
|
||||
) : SpaceService {
|
||||
private val _spaceRoomsFlow = MutableSharedFlow<List<SpaceRoom>>()
|
||||
override val spaceRoomsFlow: SharedFlow<List<SpaceRoom>>
|
||||
@@ -36,6 +38,14 @@ class FakeSpaceService(
|
||||
return joinedSpacesResult()
|
||||
}
|
||||
|
||||
override suspend fun joinedParents(spaceId: RoomId): Result<List<SpaceRoom>> {
|
||||
return joinedParentsResult(spaceId)
|
||||
}
|
||||
|
||||
override suspend fun getSpaceRoom(spaceId: RoomId): SpaceRoom? {
|
||||
return getSpaceRoomResult(spaceId)
|
||||
}
|
||||
|
||||
override fun spaceRoomList(id: RoomId): SpaceRoomList {
|
||||
return spaceRoomListResult(id)
|
||||
}
|
||||
|
||||
@@ -435,11 +435,6 @@ Kas sa oled kindel, et soovid jätkata?"</string>
|
||||
<string name="screen_create_poll_options_section_title">"Valikud"</string>
|
||||
<string name="screen_create_poll_remove_accessibility_label">"Kustuta: %1$s"</string>
|
||||
<string name="screen_create_poll_settings_section_title">"Seadistused"</string>
|
||||
<string name="screen_manage_authorized_spaces_header">"Kogukonnad, milles on võimalik jututoaga liituda ilma kutseta."</string>
|
||||
<string name="screen_manage_authorized_spaces_title">"Halda kogukondi"</string>
|
||||
<string name="screen_manage_authorized_spaces_unknown_space">"(Tundmatu kogukond)"</string>
|
||||
<string name="screen_manage_authorized_spaces_unknown_spaces_section_title">"Muud kogukonnad, mille liige sa ei ole"</string>
|
||||
<string name="screen_manage_authorized_spaces_your_spaces_section_title">"Sinu kogukonnad"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Meediafaili valimine ei õnnestunud. Palun proovi uuesti."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_description">"Siia lisamiseks vajuta sõnumil ja vali „%1$s“."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_headline">"Et olulisi sõnumeid oleks lihtsam leida, tõsta nad esile"</string>
|
||||
|
||||
@@ -435,11 +435,6 @@ Raison : %1$s."</string>
|
||||
<string name="screen_create_poll_options_section_title">"Options"</string>
|
||||
<string name="screen_create_poll_remove_accessibility_label">"Supprimer %1$s"</string>
|
||||
<string name="screen_create_poll_settings_section_title">"Paramètres"</string>
|
||||
<string name="screen_manage_authorized_spaces_header">"Espaces où les membres peuvent rejoindre le salon sans invitation."</string>
|
||||
<string name="screen_manage_authorized_spaces_title">"Gérer les espaces"</string>
|
||||
<string name="screen_manage_authorized_spaces_unknown_space">"(Espace inconnu)"</string>
|
||||
<string name="screen_manage_authorized_spaces_unknown_spaces_section_title">"Autres espaces dont vous n’êtes pas membre"</string>
|
||||
<string name="screen_manage_authorized_spaces_your_spaces_section_title">"Vos espaces"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Échec de la sélection du média, veuillez réessayer."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_description">"Cliquez (clic long) sur un message et choisissez « %1$s » pour qu‘il apparaisse ici."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_headline">"Épinglez les messages importants pour leur donner plus de visibilité"</string>
|
||||
|
||||
@@ -443,11 +443,6 @@ Jeste li sigurni da želite nastaviti?"</string>
|
||||
<string name="screen_create_poll_options_section_title">"Mogućnosti"</string>
|
||||
<string name="screen_create_poll_remove_accessibility_label">"Ukloni %1$s"</string>
|
||||
<string name="screen_create_poll_settings_section_title">"Postavke"</string>
|
||||
<string name="screen_manage_authorized_spaces_header">"Prostori u kojima se članovi mogu pridružiti sobi bez pozivnice."</string>
|
||||
<string name="screen_manage_authorized_spaces_title">"Upravljaj prostorima"</string>
|
||||
<string name="screen_manage_authorized_spaces_unknown_space">"(nepoznati prostor)"</string>
|
||||
<string name="screen_manage_authorized_spaces_unknown_spaces_section_title">"Drugi prostori čiji niste član"</string>
|
||||
<string name="screen_manage_authorized_spaces_your_spaces_section_title">"Vaši prostori"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Odabir medija nije uspio, pokušajte ponovno."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_description">"Pritisnite poruku i odaberite “%1$s” kako biste uključili ovdje."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_headline">"Prikvačite važne poruke kako bi ih se lakše moglo pronaći"</string>
|
||||
|
||||
@@ -434,11 +434,6 @@ Você tem certeza de que deseja continuar?"</string>
|
||||
<string name="screen_create_poll_options_section_title">"Opções"</string>
|
||||
<string name="screen_create_poll_remove_accessibility_label">"Remover %1$s"</string>
|
||||
<string name="screen_create_poll_settings_section_title">"Configurações"</string>
|
||||
<string name="screen_manage_authorized_spaces_header">"Os espaços dos quais os membros podem entrar na sala sem um convite."</string>
|
||||
<string name="screen_manage_authorized_spaces_title">"Gerenciar espaços"</string>
|
||||
<string name="screen_manage_authorized_spaces_unknown_space">"(Espaço desconhecido)"</string>
|
||||
<string name="screen_manage_authorized_spaces_unknown_spaces_section_title">"Outros espaços dos quais você não é um membro"</string>
|
||||
<string name="screen_manage_authorized_spaces_your_spaces_section_title">"Seus espaços"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Falha ao selecionar a mídia, tente novamente."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_description">"Pressione em uma mensagem e escolha \"%1$s\" para incluir aqui."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_headline">"Fixe mensagens importantes para que elas possam ser facilmente descobertas"</string>
|
||||
|
||||
@@ -442,11 +442,6 @@ Sunteți sigur că doriți să continuați?"</string>
|
||||
<string name="screen_create_poll_options_section_title">"Opțiuni"</string>
|
||||
<string name="screen_create_poll_remove_accessibility_label">"Ștergeți %1$s"</string>
|
||||
<string name="screen_create_poll_settings_section_title">"Setări"</string>
|
||||
<string name="screen_manage_authorized_spaces_header">"Spațile din care membrii se pot alătura camerei fără invitație."</string>
|
||||
<string name="screen_manage_authorized_spaces_title">"Gestionați spațiile"</string>
|
||||
<string name="screen_manage_authorized_spaces_unknown_space">"(Spațiu necunoscut)"</string>
|
||||
<string name="screen_manage_authorized_spaces_unknown_spaces_section_title">"Alte spații din care nu faceți parte"</string>
|
||||
<string name="screen_manage_authorized_spaces_your_spaces_section_title">"Spațiile dumneavoastră"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Selectarea fișierelor media a eșuat, încercați din nou."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_description">"Apăsați pe un mesaj și alegeți \"%1$s\" pentru a-l include aici."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_headline">"Fixați mesajele importante, astfel încât să poată fi descoperite cu ușurință"</string>
|
||||
|
||||
@@ -439,11 +439,6 @@ Naozaj chcete pokračovať?"</string>
|
||||
<string name="screen_create_poll_options_section_title">"Možnosti"</string>
|
||||
<string name="screen_create_poll_remove_accessibility_label">"Odstrániť %1$s"</string>
|
||||
<string name="screen_create_poll_settings_section_title">"Nastavenia"</string>
|
||||
<string name="screen_manage_authorized_spaces_header">"Priestory, kde sa členovia môžu pripojiť k miestnosti bez pozvania."</string>
|
||||
<string name="screen_manage_authorized_spaces_title">"Spravovať priestory"</string>
|
||||
<string name="screen_manage_authorized_spaces_unknown_space">"(Neznámy priestor)"</string>
|
||||
<string name="screen_manage_authorized_spaces_unknown_spaces_section_title">"Iné priestory, ktorých nie ste členom"</string>
|
||||
<string name="screen_manage_authorized_spaces_your_spaces_section_title">"Vaše priestory"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Nepodarilo sa vybrať médium, skúste to prosím znova."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_description">"Stlačte správu a vyberte možnosť „%1$s“, ktorú chcete zahrnúť sem."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_headline">"Pripnite dôležité správy, aby sa dali ľahko nájsť"</string>
|
||||
|
||||
@@ -435,11 +435,6 @@ Are you sure you want to continue?"</string>
|
||||
<string name="screen_create_poll_options_section_title">"Options"</string>
|
||||
<string name="screen_create_poll_remove_accessibility_label">"Remove %1$s"</string>
|
||||
<string name="screen_create_poll_settings_section_title">"Settings"</string>
|
||||
<string name="screen_manage_authorized_spaces_header">"Spaces where members can join the room without an invitation."</string>
|
||||
<string name="screen_manage_authorized_spaces_title">"Manage spaces"</string>
|
||||
<string name="screen_manage_authorized_spaces_unknown_space">"(Unknown space)"</string>
|
||||
<string name="screen_manage_authorized_spaces_unknown_spaces_section_title">"Other spaces you’re not a member of"</string>
|
||||
<string name="screen_manage_authorized_spaces_your_spaces_section_title">"Your spaces"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Failed selecting media, please try again."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_description">"Press on a message and choose “%1$s” to include here."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_headline">"Pin important messages so that they can be easily discovered"</string>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -403,7 +403,8 @@
|
||||
"name" : ":features:securityandprivacy:impl",
|
||||
"includeRegex" : [
|
||||
"screen\\.edit_room_address\\..*",
|
||||
"screen\\.security_and_privacy\\..*"
|
||||
"screen\\.security_and_privacy\\..*",
|
||||
"screen\\.manage_authorized_spaces\\..*"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user