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:
commit
9a40a6404e
85 changed files with 2535 additions and 571 deletions
|
|
@ -9,6 +9,7 @@
|
||||||
package io.element.android.features.securityandprivacy.impl
|
package io.element.android.features.securityandprivacy.impl
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.lifecycle.Lifecycle
|
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.SecurityAndPrivacyEntryPoint
|
||||||
import io.element.android.features.securityandprivacy.api.securityAndPrivacyPermissions
|
import io.element.android.features.securityandprivacy.api.securityAndPrivacyPermissions
|
||||||
import io.element.android.features.securityandprivacy.impl.editroomaddress.EditRoomAddressNode
|
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.features.securityandprivacy.impl.root.SecurityAndPrivacyNode
|
||||||
import io.element.android.libraries.architecture.BackstackView
|
import io.element.android.libraries.architecture.BackstackView
|
||||||
import io.element.android.libraries.architecture.BaseFlowNode
|
import io.element.android.libraries.architecture.BaseFlowNode
|
||||||
|
|
@ -58,10 +60,15 @@ class SecurityAndPrivacyFlowNode(
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object EditRoomAddress : NavTarget
|
data object EditRoomAddress : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data object ManageAuthorizedSpaces : NavTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
private val callback: SecurityAndPrivacyEntryPoint.Callback = callback()
|
private val callback: SecurityAndPrivacyEntryPoint.Callback = callback()
|
||||||
private val navigator = BackstackSecurityAndPrivacyNavigator(callback, backstack)
|
|
||||||
|
@VisibleForTesting
|
||||||
|
val navigator = BackstackSecurityAndPrivacyNavigator(callback, backstack)
|
||||||
|
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
super.onBuilt()
|
super.onBuilt()
|
||||||
|
|
@ -89,6 +96,9 @@ class SecurityAndPrivacyFlowNode(
|
||||||
NavTarget.EditRoomAddress -> {
|
NavTarget.EditRoomAddress -> {
|
||||||
createNode<EditRoomAddressNode>(buildContext, plugins = listOf(navigator))
|
createNode<EditRoomAddressNode>(buildContext, plugins = listOf(navigator))
|
||||||
}
|
}
|
||||||
|
NavTarget.ManageAuthorizedSpaces -> {
|
||||||
|
createNode<ManageAuthorizedSpacesNode>(buildContext, plugins = listOf(navigator))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ interface SecurityAndPrivacyNavigator : Plugin {
|
||||||
fun onDone()
|
fun onDone()
|
||||||
fun openEditRoomAddress()
|
fun openEditRoomAddress()
|
||||||
fun closeEditRoomAddress()
|
fun closeEditRoomAddress()
|
||||||
|
fun openManageAuthorizedSpaces()
|
||||||
|
fun closeManageAuthorizedSpaces()
|
||||||
}
|
}
|
||||||
|
|
||||||
class BackstackSecurityAndPrivacyNavigator(
|
class BackstackSecurityAndPrivacyNavigator(
|
||||||
|
|
@ -35,4 +37,12 @@ class BackstackSecurityAndPrivacyNavigator(
|
||||||
override fun closeEditRoomAddress() {
|
override fun closeEditRoomAddress() {
|
||||||
backStack.pop()
|
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 {
|
sealed interface SecurityAndPrivacyEvent {
|
||||||
data object EditRoomAddress : SecurityAndPrivacyEvent
|
data object EditRoomAddress : SecurityAndPrivacyEvent
|
||||||
|
data object ManageAuthorizedSpaces : SecurityAndPrivacyEvent
|
||||||
data object Save : SecurityAndPrivacyEvent
|
data object Save : SecurityAndPrivacyEvent
|
||||||
data object Exit : SecurityAndPrivacyEvent
|
data object Exit : SecurityAndPrivacyEvent
|
||||||
data object DismissExitConfirmation : SecurityAndPrivacyEvent
|
data object DismissExitConfirmation : SecurityAndPrivacyEvent
|
||||||
data class ChangeRoomAccess(val roomAccess: SecurityAndPrivacyRoomAccess) : 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 ToggleEncryptionState : SecurityAndPrivacyEvent
|
||||||
data object CancelEnableEncryption : SecurityAndPrivacyEvent
|
data object CancelEnableEncryption : SecurityAndPrivacyEvent
|
||||||
data object ConfirmEnableEncryption : SecurityAndPrivacyEvent
|
data object ConfirmEnableEncryption : SecurityAndPrivacyEvent
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
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.api.securityAndPrivacyPermissions
|
||||||
import io.element.android.features.securityandprivacy.impl.SecurityAndPrivacyNavigator
|
import io.element.android.features.securityandprivacy.impl.SecurityAndPrivacyNavigator
|
||||||
import io.element.android.features.securityandprivacy.impl.editroomaddress.matchesServer
|
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.AsyncAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.architecture.Presenter
|
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.JoinedRoom
|
||||||
import io.element.android.libraries.matrix.api.room.RoomInfo
|
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.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.join.JoinRule
|
||||||
import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState
|
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.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.CoroutineScope
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@AssistedInject
|
@AssistedInject
|
||||||
class SecurityAndPrivacyPresenter(
|
class SecurityAndPrivacyPresenter(
|
||||||
@Assisted private val navigator: SecurityAndPrivacyNavigator,
|
@Assisted private val navigator: SecurityAndPrivacyNavigator,
|
||||||
|
private val spaceSelectionStateHolder: SpaceSelectionStateHolder,
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
private val room: JoinedRoom,
|
private val room: JoinedRoom,
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
) : Presenter<SecurityAndPrivacyState> {
|
) : Presenter<SecurityAndPrivacyState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(navigator: SecurityAndPrivacyNavigator): SecurityAndPrivacyPresenter
|
fun create(
|
||||||
|
navigator: SecurityAndPrivacyNavigator,
|
||||||
|
): SecurityAndPrivacyPresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -65,6 +78,10 @@ class SecurityAndPrivacyPresenter(
|
||||||
val isKnockEnabled by remember {
|
val isKnockEnabled by remember {
|
||||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock)
|
featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock)
|
||||||
}.collectAsState(false)
|
}.collectAsState(false)
|
||||||
|
val isSpaceSettingsEnabled by remember {
|
||||||
|
featureFlagService.isFeatureEnabledFlow(FeatureFlags.SpaceSettings)
|
||||||
|
}.collectAsState(false)
|
||||||
|
|
||||||
val saveAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
val saveAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||||
val homeserverName = remember { matrixClient.userIdServerName() }
|
val homeserverName = remember { matrixClient.userIdServerName() }
|
||||||
val roomInfo by room.roomInfoFlow.collectAsState()
|
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)
|
mutableStateOf(savedSettings.roomAccess)
|
||||||
}
|
}
|
||||||
var editedHistoryVisibility by remember(savedSettings.historyVisibility) {
|
var editedHistoryVisibility by remember(savedSettings.historyVisibility) {
|
||||||
|
|
@ -99,13 +116,44 @@ class SecurityAndPrivacyPresenter(
|
||||||
mutableStateOf(savedIsVisibleInRoomDirectory.value)
|
mutableStateOf(savedIsVisibleInRoomDirectory.value)
|
||||||
}
|
}
|
||||||
val editedSettings = SecurityAndPrivacySettings(
|
val editedSettings = SecurityAndPrivacySettings(
|
||||||
roomAccess = editedRoomAccess,
|
roomAccess = editedRoomAccess.value,
|
||||||
isEncrypted = editedIsEncrypted,
|
isEncrypted = editedIsEncrypted,
|
||||||
isVisibleInRoomDirectory = editedVisibleInRoomDirectory,
|
isVisibleInRoomDirectory = editedVisibleInRoomDirectory,
|
||||||
historyVisibility = editedHistoryVisibility,
|
historyVisibility = editedHistoryVisibility,
|
||||||
address = savedSettings.address,
|
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) }
|
var showEnableEncryptionConfirmation by remember(savedSettings.isEncrypted) { mutableStateOf(false) }
|
||||||
val permissions by room.permissionsAsState(SecurityAndPrivacyPermissions.DEFAULT) { perms ->
|
val permissions by room.permissionsAsState(SecurityAndPrivacyPermissions.DEFAULT) { perms ->
|
||||||
perms.securityAndPrivacyPermissions()
|
perms.securityAndPrivacyPermissions()
|
||||||
|
|
@ -122,7 +170,7 @@ class SecurityAndPrivacyPresenter(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is SecurityAndPrivacyEvent.ChangeRoomAccess -> {
|
is SecurityAndPrivacyEvent.ChangeRoomAccess -> {
|
||||||
editedRoomAccess = event.roomAccess
|
editedRoomAccess.value = event.roomAccess
|
||||||
}
|
}
|
||||||
is SecurityAndPrivacyEvent.ToggleEncryptionState -> {
|
is SecurityAndPrivacyEvent.ToggleEncryptionState -> {
|
||||||
if (editedIsEncrypted) {
|
if (editedIsEncrypted) {
|
||||||
|
|
@ -161,6 +209,27 @@ class SecurityAndPrivacyPresenter(
|
||||||
SecurityAndPrivacyEvent.DismissExitConfirmation -> {
|
SecurityAndPrivacyEvent.DismissExitConfirmation -> {
|
||||||
saveAction.value = AsyncAction.Uninitialized
|
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,
|
saveAction = saveAction.value,
|
||||||
permissions = permissions,
|
permissions = permissions,
|
||||||
isSpace = roomInfo.isSpace,
|
isSpace = roomInfo.isSpace,
|
||||||
|
isSpaceSettingsEnabled = isSpaceSettingsEnabled,
|
||||||
|
selectableJoinedSpaces = selectableJoinedSpaces,
|
||||||
|
spaceSelectionMode = spaceSelectionMode,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Revert changes that the user is not allowed to make anymore
|
// Revert changes that the user is not allowed to make anymore
|
||||||
LaunchedEffect(permissions, state.editedSettings.roomAccess) {
|
LaunchedEffect(permissions, state.editedSettings.roomAccess) {
|
||||||
if (!state.showRoomAccessSection) {
|
if (!state.showRoomAccessSection) {
|
||||||
editedRoomAccess = savedSettings.roomAccess
|
editedRoomAccess.value = savedSettings.roomAccess
|
||||||
}
|
}
|
||||||
if (!state.showEncryptionSection) {
|
if (!state.showEncryptionSection) {
|
||||||
editedIsEncrypted = savedSettings.isEncrypted
|
editedIsEncrypted = savedSettings.isEncrypted
|
||||||
|
|
@ -202,6 +274,110 @@ class SecurityAndPrivacyPresenter(
|
||||||
return state
|
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 {
|
private fun CoroutineScope.isRoomVisibleInRoomDirectory(isRoomVisible: MutableState<AsyncData<Boolean>>) = launch {
|
||||||
isRoomVisible.runUpdatingState {
|
isRoomVisible.runUpdatingState {
|
||||||
room.getRoomVisibility().map { it == RoomVisibility.Public }
|
room.getRoomVisibility().map { it == RoomVisibility.Public }
|
||||||
|
|
@ -242,6 +418,7 @@ class SecurityAndPrivacyPresenter(
|
||||||
// the room should be automatically made invisible (private) in the room directory.
|
// the room should be automatically made invisible (private) in the room directory.
|
||||||
val editedIsVisibleInRoomDirectory = when (editedSettings.roomAccess) {
|
val editedIsVisibleInRoomDirectory = when (editedSettings.roomAccess) {
|
||||||
SecurityAndPrivacyRoomAccess.AskToJoin,
|
SecurityAndPrivacyRoomAccess.AskToJoin,
|
||||||
|
is SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember,
|
||||||
SecurityAndPrivacyRoomAccess.Anyone -> editedSettings.isVisibleInRoomDirectory.dataOrNull()
|
SecurityAndPrivacyRoomAccess.Anyone -> editedSettings.isVisibleInRoomDirectory.dataOrNull()
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
|
@ -279,8 +456,19 @@ class SecurityAndPrivacyPresenter(
|
||||||
private fun JoinRule?.map(): SecurityAndPrivacyRoomAccess {
|
private fun JoinRule?.map(): SecurityAndPrivacyRoomAccess {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
JoinRule.Public -> SecurityAndPrivacyRoomAccess.Anyone
|
JoinRule.Public -> SecurityAndPrivacyRoomAccess.Anyone
|
||||||
JoinRule.Knock, is JoinRule.KnockRestricted -> SecurityAndPrivacyRoomAccess.AskToJoin
|
JoinRule.Knock -> SecurityAndPrivacyRoomAccess.AskToJoin
|
||||||
is JoinRule.Restricted -> SecurityAndPrivacyRoomAccess.SpaceMember
|
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
|
JoinRule.Invite -> SecurityAndPrivacyRoomAccess.InviteOnly
|
||||||
// All other cases are not supported so we default to InviteOnly
|
// All other cases are not supported so we default to InviteOnly
|
||||||
is JoinRule.Custom,
|
is JoinRule.Custom,
|
||||||
|
|
@ -294,8 +482,12 @@ private fun SecurityAndPrivacyRoomAccess.map(): JoinRule? {
|
||||||
SecurityAndPrivacyRoomAccess.Anyone -> JoinRule.Public
|
SecurityAndPrivacyRoomAccess.Anyone -> JoinRule.Public
|
||||||
SecurityAndPrivacyRoomAccess.AskToJoin -> JoinRule.Knock
|
SecurityAndPrivacyRoomAccess.AskToJoin -> JoinRule.Knock
|
||||||
SecurityAndPrivacyRoomAccess.InviteOnly -> JoinRule.Private
|
SecurityAndPrivacyRoomAccess.InviteOnly -> JoinRule.Private
|
||||||
// SpaceMember can't be selected in the ui
|
is SecurityAndPrivacyRoomAccess.SpaceMember -> JoinRule.Restricted(
|
||||||
SecurityAndPrivacyRoomAccess.SpaceMember -> null
|
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
|
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.api.SecurityAndPrivacyPermissions
|
||||||
|
import io.element.android.features.securityandprivacy.impl.R
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
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
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
data class SecurityAndPrivacyState(
|
data class SecurityAndPrivacyState(
|
||||||
|
|
@ -20,12 +28,42 @@ data class SecurityAndPrivacyState(
|
||||||
val editedSettings: SecurityAndPrivacySettings,
|
val editedSettings: SecurityAndPrivacySettings,
|
||||||
val homeserverName: String,
|
val homeserverName: String,
|
||||||
val showEnableEncryptionConfirmation: Boolean,
|
val showEnableEncryptionConfirmation: Boolean,
|
||||||
val isKnockEnabled: Boolean,
|
private val isKnockEnabled: Boolean,
|
||||||
|
private val isSpaceSettingsEnabled: Boolean,
|
||||||
val saveAction: AsyncAction<Unit>,
|
val saveAction: AsyncAction<Unit>,
|
||||||
val isSpace: Boolean,
|
val isSpace: Boolean,
|
||||||
private val permissions: SecurityAndPrivacyPermissions,
|
private val permissions: SecurityAndPrivacyPermissions,
|
||||||
|
private val selectableJoinedSpaces: ImmutableSet<SpaceRoom>,
|
||||||
|
private val spaceSelectionMode: SpaceSelectionMode,
|
||||||
val eventSink: (SecurityAndPrivacyEvent) -> Unit
|
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
|
val canBeSaved = savedSettings != editedSettings
|
||||||
|
|
||||||
// Logic is in https://github.com/element-hq/element-meta/issues/3029
|
// 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 showHistoryVisibilitySection = permissions.canChangeHistoryVisibility && !isSpace
|
||||||
val showEncryptionSection = permissions.canChangeEncryption && !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(
|
data class SecurityAndPrivacySettings(
|
||||||
|
|
@ -76,16 +148,31 @@ enum class SecurityAndPrivacyHistoryVisibility {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class SecurityAndPrivacyRoomAccess {
|
sealed interface SpaceSelectionMode {
|
||||||
InviteOnly,
|
data object None : SpaceSelectionMode
|
||||||
AskToJoin,
|
data class Single(val spaceId: RoomId, val spaceRoom: SpaceRoom?) : SpaceSelectionMode
|
||||||
Anyone,
|
data object Multiple : SpaceSelectionMode
|
||||||
SpaceMember;
|
}
|
||||||
|
|
||||||
|
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 {
|
fun canConfigureRoomVisibility(): Boolean {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
InviteOnly, SpaceMember -> false
|
InviteOnly, is SpaceMember -> false
|
||||||
AskToJoin, Anyone -> true
|
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.features.securityandprivacy.api.SecurityAndPrivacyPermissions
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
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> {
|
open class SecurityAndPrivacyStateProvider : PreviewParameterProvider<SecurityAndPrivacyState> {
|
||||||
override val values: Sequence<SecurityAndPrivacyState>
|
override val values: Sequence<SecurityAndPrivacyState>
|
||||||
|
|
@ -61,11 +64,27 @@ private fun commonSecurityAndPrivacyStates(isSpace: Boolean): Sequence<SecurityA
|
||||||
),
|
),
|
||||||
aSecurityAndPrivacyState(
|
aSecurityAndPrivacyState(
|
||||||
savedSettings = aSecurityAndPrivacySettings(
|
savedSettings = aSecurityAndPrivacySettings(
|
||||||
roomAccess = SecurityAndPrivacyRoomAccess.SpaceMember
|
roomAccess = SecurityAndPrivacyRoomAccess.SpaceMember(persistentListOf())
|
||||||
),
|
),
|
||||||
|
spaceSelectionMode = SpaceSelectionMode.Multiple,
|
||||||
isSpace = isSpace,
|
isSpace = isSpace,
|
||||||
isKnockEnabled = false,
|
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(
|
aSecurityAndPrivacyState(
|
||||||
editedSettings = aSecurityAndPrivacySettings(
|
editedSettings = aSecurityAndPrivacySettings(
|
||||||
roomAccess = SecurityAndPrivacyRoomAccess.Anyone,
|
roomAccess = SecurityAndPrivacyRoomAccess.Anyone,
|
||||||
|
|
@ -117,6 +136,9 @@ fun aSecurityAndPrivacyState(
|
||||||
),
|
),
|
||||||
isKnockEnabled: Boolean = true,
|
isKnockEnabled: Boolean = true,
|
||||||
isSpace: Boolean = false,
|
isSpace: Boolean = false,
|
||||||
|
selectableJoinedSpaces: Set<SpaceRoom> = emptySet(),
|
||||||
|
spaceSelectionMode: SpaceSelectionMode = SpaceSelectionMode.None,
|
||||||
|
isSpaceSettingsEnabled: Boolean = true,
|
||||||
eventSink: (SecurityAndPrivacyEvent) -> Unit = {}
|
eventSink: (SecurityAndPrivacyEvent) -> Unit = {}
|
||||||
) = SecurityAndPrivacyState(
|
) = SecurityAndPrivacyState(
|
||||||
editedSettings = editedSettings,
|
editedSettings = editedSettings,
|
||||||
|
|
@ -127,5 +149,8 @@ fun aSecurityAndPrivacyState(
|
||||||
isKnockEnabled = isKnockEnabled,
|
isKnockEnabled = isKnockEnabled,
|
||||||
permissions = permissions,
|
permissions = permissions,
|
||||||
isSpace = isSpace,
|
isSpace = isSpace,
|
||||||
|
selectableJoinedSpaces = selectableJoinedSpaces.toImmutableSet(),
|
||||||
|
spaceSelectionMode = spaceSelectionMode,
|
||||||
|
isSpaceSettingsEnabled = isSpaceSettingsEnabled,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -90,11 +90,8 @@ fun SecurityAndPrivacyView(
|
||||||
) {
|
) {
|
||||||
if (state.showRoomAccessSection) {
|
if (state.showRoomAccessSection) {
|
||||||
RoomAccessSection(
|
RoomAccessSection(
|
||||||
|
state = state,
|
||||||
modifier = Modifier.padding(top = 24.dp),
|
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) {
|
if (state.showRoomVisibilitySections) {
|
||||||
|
|
@ -208,12 +205,27 @@ private fun SecurityAndPrivacySection(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RoomAccessSection(
|
private fun RoomAccessSection(
|
||||||
edited: SecurityAndPrivacyRoomAccess,
|
state: SecurityAndPrivacyState,
|
||||||
saved: SecurityAndPrivacyRoomAccess,
|
|
||||||
isKnockEnabled: Boolean,
|
|
||||||
onSelectOption: (SecurityAndPrivacyRoomAccess) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
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(
|
SecurityAndPrivacySection(
|
||||||
title = stringResource(R.string.screen_security_and_privacy_room_access_section_header),
|
title = stringResource(R.string.screen_security_and_privacy_room_access_section_header),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|
@ -225,29 +237,36 @@ private fun RoomAccessSection(
|
||||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Public())),
|
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Public())),
|
||||||
onClick = { onSelectOption(SecurityAndPrivacyRoomAccess.Anyone) },
|
onClick = { onSelectOption(SecurityAndPrivacyRoomAccess.Anyone) },
|
||||||
)
|
)
|
||||||
// Show space member option, but disabled as we don't support this option for now.
|
if (state.showSpaceMemberOption) {
|
||||||
if (saved == SecurityAndPrivacyRoomAccess.SpaceMember) {
|
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text(text = stringResource(R.string.screen_security_and_privacy_room_access_space_members_option_title)) },
|
headlineContent = { Text(text = stringResource(R.string.screen_security_and_privacy_room_access_space_members_option_title)) },
|
||||||
supportingContent = {
|
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())),
|
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Space())),
|
||||||
enabled = false,
|
onClick = ::onSpaceMemberAccessClick,
|
||||||
|
enabled = state.isSpaceMemberSelectable,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Show Ask to join option in two cases:
|
if (state.showAskToJoinOption) {
|
||||||
// - the Knock FF is enabled
|
|
||||||
// - AskToJoin is the current saved value
|
|
||||||
if (saved == SecurityAndPrivacyRoomAccess.AskToJoin || isKnockEnabled) {
|
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text(text = stringResource(R.string.screen_security_and_privacy_ask_to_join_option_title)) },
|
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)) },
|
supportingContent = { Text(text = stringResource(R.string.screen_security_and_privacy_ask_to_join_option_description)) },
|
||||||
trailingContent = ListItemContent.RadioButton(selected = edited == SecurityAndPrivacyRoomAccess.AskToJoin),
|
trailingContent = ListItemContent.RadioButton(selected = edited == SecurityAndPrivacyRoomAccess.AskToJoin),
|
||||||
onClick = { onSelectOption(SecurityAndPrivacyRoomAccess.AskToJoin) },
|
onClick = { onSelectOption(SecurityAndPrivacyRoomAccess.AskToJoin) },
|
||||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.UserAdd())),
|
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(
|
ListItem(
|
||||||
|
|
@ -257,6 +276,20 @@ private fun RoomAccessSection(
|
||||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Lock())),
|
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Lock())),
|
||||||
onClick = { onSelectOption(SecurityAndPrivacyRoomAccess.InviteOnly) },
|
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">
|
<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_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_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_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_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>
|
<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">
|
<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_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_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_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_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>
|
<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">
|
<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_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_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_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_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>
|
<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">
|
<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_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_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_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_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>
|
<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">
|
<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_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_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_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_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>
|
<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">
|
<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_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_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_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_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>
|
<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">
|
<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_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_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_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_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>
|
<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 onDoneLambda: () -> Unit = { lambdaError() },
|
||||||
private val openEditRoomAddressLambda: () -> Unit = { lambdaError() },
|
private val openEditRoomAddressLambda: () -> Unit = { lambdaError() },
|
||||||
private val closeEditRoomAddressLambda: () -> Unit = { lambdaError() },
|
private val closeEditRoomAddressLambda: () -> Unit = { lambdaError() },
|
||||||
|
private val openManageAuthorizedSpacesLambda: () -> Unit = { lambdaError() },
|
||||||
|
private val closeManageAuthorizedSpacesLambda: () -> Unit = { lambdaError() },
|
||||||
) : SecurityAndPrivacyNavigator {
|
) : SecurityAndPrivacyNavigator {
|
||||||
override fun onDone() {
|
override fun onDone() {
|
||||||
onDoneLambda()
|
onDoneLambda()
|
||||||
|
|
@ -26,4 +28,12 @@ class FakeSecurityAndPrivacyNavigator(
|
||||||
override fun closeEditRoomAddress() {
|
override fun closeEditRoomAddress() {
|
||||||
closeEditRoomAddressLambda()
|
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 (c) 2026 Element Creations Ltd.
|
||||||
* Copyright 2025 New Vector Ltd.
|
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* 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.activity.ComponentActivity
|
||||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
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.onNodeWithText
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyEvent
|
import io.element.android.features.securityandprivacy.impl.R
|
||||||
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.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
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.libraries.ui.strings.CommonStrings
|
||||||
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
||||||
import io.element.android.tests.testutils.EventsRecorder
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
import io.element.android.tests.testutils.clickOn
|
import io.element.android.tests.testutils.clickOn
|
||||||
import io.element.android.tests.testutils.pressBack
|
import io.element.android.tests.testutils.pressBack
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.rules.TestRule
|
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)
|
rule.clickOn(R.string.screen_security_and_privacy_enable_encryption_alert_confirm_button_title)
|
||||||
recorder.assertSingle(SecurityAndPrivacyEvent.ConfirmEnableEncryption)
|
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(
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSecurityAndPrivacyView(
|
||||||
|
|
@ -15,6 +15,10 @@ interface SpaceService {
|
||||||
val spaceRoomsFlow: SharedFlow<List<SpaceRoom>>
|
val spaceRoomsFlow: SharedFlow<List<SpaceRoom>>
|
||||||
suspend fun joinedSpaces(): Result<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 spaceRoomList(id: RoomId): SpaceRoomList
|
||||||
|
|
||||||
fun getLeaveSpaceHandle(spaceId: RoomId): LeaveSpaceHandle
|
fun getLeaveSpaceHandle(spaceId: RoomId): LeaveSpaceHandle
|
||||||
|
|
|
||||||
|
|
@ -51,13 +51,28 @@ class RustSpaceService(
|
||||||
|
|
||||||
override suspend fun joinedSpaces(): Result<List<SpaceRoom>> = withContext(sessionDispatcher) {
|
override suspend fun joinedSpaces(): Result<List<SpaceRoom>> = withContext(sessionDispatcher) {
|
||||||
runCatchingExceptions {
|
runCatchingExceptions {
|
||||||
innerSpaceService.topLevelJoinedSpaces()
|
innerSpaceService
|
||||||
.map {
|
.topLevelJoinedSpaces()
|
||||||
it.let(spaceRoomMapper::map)
|
.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 {
|
override fun spaceRoomList(id: RoomId): SpaceRoomList {
|
||||||
val childCoroutineScope = sessionCoroutineScope.childScope(sessionDispatcher, "SpaceRoomListScope-$this")
|
val childCoroutineScope = sessionCoroutineScope.childScope(sessionDispatcher, "SpaceRoomListScope-$this")
|
||||||
return RustSpaceRoomList(
|
return RustSpaceRoomList(
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ class FakeSpaceService(
|
||||||
private val joinedSpacesResult: () -> Result<List<SpaceRoom>> = { lambdaError() },
|
private val joinedSpacesResult: () -> Result<List<SpaceRoom>> = { lambdaError() },
|
||||||
private val spaceRoomListResult: (RoomId) -> SpaceRoomList = { lambdaError() },
|
private val spaceRoomListResult: (RoomId) -> SpaceRoomList = { lambdaError() },
|
||||||
private val leaveSpaceHandleResult: (RoomId) -> LeaveSpaceHandle = { lambdaError() },
|
private val leaveSpaceHandleResult: (RoomId) -> LeaveSpaceHandle = { lambdaError() },
|
||||||
|
private val joinedParentsResult: (RoomId) -> Result<List<SpaceRoom>> = { lambdaError() },
|
||||||
|
private val getSpaceRoomResult: (RoomId) -> SpaceRoom? = { lambdaError() },
|
||||||
) : SpaceService {
|
) : SpaceService {
|
||||||
private val _spaceRoomsFlow = MutableSharedFlow<List<SpaceRoom>>()
|
private val _spaceRoomsFlow = MutableSharedFlow<List<SpaceRoom>>()
|
||||||
override val spaceRoomsFlow: SharedFlow<List<SpaceRoom>>
|
override val spaceRoomsFlow: SharedFlow<List<SpaceRoom>>
|
||||||
|
|
@ -36,6 +38,14 @@ class FakeSpaceService(
|
||||||
return joinedSpacesResult()
|
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 {
|
override fun spaceRoomList(id: RoomId): SpaceRoomList {
|
||||||
return spaceRoomListResult(id)
|
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_options_section_title">"Valikud"</string>
|
||||||
<string name="screen_create_poll_remove_accessibility_label">"Kustuta: %1$s"</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_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_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_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>
|
<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_options_section_title">"Options"</string>
|
||||||
<string name="screen_create_poll_remove_accessibility_label">"Supprimer %1$s"</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_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_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_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>
|
<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_options_section_title">"Mogućnosti"</string>
|
||||||
<string name="screen_create_poll_remove_accessibility_label">"Ukloni %1$s"</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_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_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_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>
|
<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_options_section_title">"Opções"</string>
|
||||||
<string name="screen_create_poll_remove_accessibility_label">"Remover %1$s"</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_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_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_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>
|
<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_options_section_title">"Opțiuni"</string>
|
||||||
<string name="screen_create_poll_remove_accessibility_label">"Ștergeți %1$s"</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_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_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_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>
|
<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_options_section_title">"Možnosti"</string>
|
||||||
<string name="screen_create_poll_remove_accessibility_label">"Odstrániť %1$s"</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_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_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_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>
|
<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_options_section_title">"Options"</string>
|
||||||
<string name="screen_create_poll_remove_accessibility_label">"Remove %1$s"</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_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_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_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>
|
<string name="screen_pinned_timeline_empty_state_headline">"Pin important messages so that they can be easily discovered"</string>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:1e09986f8b500061cfc9108af757b581334d8babb3c5c17433dd53dd3a8f52f3
|
||||||
|
size 48709
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:1e09986f8b500061cfc9108af757b581334d8babb3c5c17433dd53dd3a8f52f3
|
||||||
|
size 48709
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:f953609cb044e439198f7072a4468c54269b646e3c39686440334903eb12797d
|
||||||
|
size 49279
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:c481049fcd33ca40aac538bf82ca56ef27cce2448c148cc45ba95493b668ef03
|
||||||
|
size 47890
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:c481049fcd33ca40aac538bf82ca56ef27cce2448c148cc45ba95493b668ef03
|
||||||
|
size 47890
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:15ed6659fbf148a23b4090ba8896bad91d82279df71552c2798755b190a0cdca
|
||||||
|
size 48369
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:bef6e6c4cdd362577cf5dc91914a84c72465014a71a08f48c1f4a526be44c85c
|
oid sha256:67bd76421dcb3e8a6c2c0a6aeda024dacb392b2f6d8bea278d6ffae7de5b4e8f
|
||||||
size 39535
|
size 20023
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:39222fd299a2dec3cc60863878906eba5c9d86100a3e4bad781633a1a0372cc5
|
oid sha256:48f50c8053f0f15228899e8bce1656a90cfaf936e6bc8ca725c676aff5578575
|
||||||
size 39874
|
size 39874
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:5c9bf91f57bb68ae3341acecd62d418d1654a8d4cf271eeda9e87bc5e4998891
|
oid sha256:bef6e6c4cdd362577cf5dc91914a84c72465014a71a08f48c1f4a526be44c85c
|
||||||
size 20225
|
size 39535
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:495fff852c9549125b3d3a681a880147f680d4604eee3cb13a8fc3dc47e7c729
|
oid sha256:39222fd299a2dec3cc60863878906eba5c9d86100a3e4bad781633a1a0372cc5
|
||||||
size 40476
|
size 39874
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:39222fd299a2dec3cc60863878906eba5c9d86100a3e4bad781633a1a0372cc5
|
oid sha256:da260572a274c3fedcc81b055e1c52b8cf7467692287aa176b0dd9171be76937
|
||||||
size 39874
|
size 25139
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:cdd805d857d590437bde9be2a7df252ff42571d3b08be8085aae81dc75e896c3
|
oid sha256:da888123f37a0dcc7d80849216044b82a442052c900d636d006020ad42469a71
|
||||||
size 40235
|
size 51849
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:507eec1e5125056ca4980ae67f904dd30bcb88ef6d66d5968aafbb3c1449e4b8
|
oid sha256:da888123f37a0dcc7d80849216044b82a442052c900d636d006020ad42469a71
|
||||||
size 33779
|
size 51849
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d300a9854096634ea0e6722db487607a75f6e0c46d7341f044a190a212d95307
|
oid sha256:495fff852c9549125b3d3a681a880147f680d4604eee3cb13a8fc3dc47e7c729
|
||||||
size 32373
|
size 40476
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b42e3df9fec259210b63431e295e8c7d1a7640149c9616b572b90c0b40392a60
|
oid sha256:39222fd299a2dec3cc60863878906eba5c9d86100a3e4bad781633a1a0372cc5
|
||||||
size 34637
|
size 39874
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3bb107a37b14dcbc6aa530a8d47f58c809df7719a0c8a416a268ffd2df165681
|
oid sha256:cdd805d857d590437bde9be2a7df252ff42571d3b08be8085aae81dc75e896c3
|
||||||
size 41602
|
size 40235
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:507eec1e5125056ca4980ae67f904dd30bcb88ef6d66d5968aafbb3c1449e4b8
|
||||||
|
size 33779
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:d300a9854096634ea0e6722db487607a75f6e0c46d7341f044a190a212d95307
|
||||||
|
size 32373
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:b42e3df9fec259210b63431e295e8c7d1a7640149c9616b572b90c0b40392a60
|
||||||
|
size 34637
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:3bb107a37b14dcbc6aa530a8d47f58c809df7719a0c8a416a268ffd2df165681
|
||||||
|
size 41602
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b67ebf4536e36571b0867e71533b2fe1953ca2b88f15fe3531f3ddb9043f69ec
|
oid sha256:64134bc23ef38b73a087ec5679784516bfd66f23dd675a3771e1aa8cf6159836
|
||||||
size 39418
|
size 44729
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e5fb16ef839b9f93caddbc984c8be260846e7fd7d68ae3b31dcc97c7644b41b4
|
oid sha256:56d1614ce5e0da0bc29f8184f045339e263087676e9d7908a94007f410924b5d
|
||||||
size 56670
|
size 59154
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9402ad99b181b99083abff78d078e9fbb010144e07f80ff995db7884a61fa853
|
oid sha256:56d1614ce5e0da0bc29f8184f045339e263087676e9d7908a94007f410924b5d
|
||||||
size 56060
|
size 59154
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:cadacec9344f3ca2c029006c620cd8c309bb7f4b38cbb7b47c7c336ca265a141
|
oid sha256:e5fb16ef839b9f93caddbc984c8be260846e7fd7d68ae3b31dcc97c7644b41b4
|
||||||
size 56424
|
size 56670
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:67bd76421dcb3e8a6c2c0a6aeda024dacb392b2f6d8bea278d6ffae7de5b4e8f
|
oid sha256:9402ad99b181b99083abff78d078e9fbb010144e07f80ff995db7884a61fa853
|
||||||
size 20023
|
size 56060
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:48f50c8053f0f15228899e8bce1656a90cfaf936e6bc8ca725c676aff5578575
|
oid sha256:cadacec9344f3ca2c029006c620cd8c309bb7f4b38cbb7b47c7c336ca265a141
|
||||||
size 39874
|
size 56424
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:342cc313605f43325a0720fe9c9fa969ed7edd41258ce0768dc624fc4e74d66d
|
oid sha256:8303e863849a00c16f00850854e8d4d7ceaf3fb097c7b20fe349127d8ed3b082
|
||||||
size 40636
|
size 20651
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:adfd4213ac36d904a8a31b1c3bf5463ba72ad4760d084a4c412370d6d51cf250
|
oid sha256:1626bffb649c204b0dcb1c92e7344d1b7f119b61ecac3c49840385f3a9a39515
|
||||||
size 40996
|
size 40998
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:4ef8e92812f6b65fc09ea4e77d44f9c32a74daebe8e2ef2a9e499588e46a43db
|
oid sha256:342cc313605f43325a0720fe9c9fa969ed7edd41258ce0768dc624fc4e74d66d
|
||||||
size 20823
|
size 40636
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:5547eb8b7f7ca596edf65bfd7703d1cb91fbf2e254374b9ea41d5c8754030e34
|
oid sha256:adfd4213ac36d904a8a31b1c3bf5463ba72ad4760d084a4c412370d6d51cf250
|
||||||
size 41628
|
size 40996
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:adfd4213ac36d904a8a31b1c3bf5463ba72ad4760d084a4c412370d6d51cf250
|
oid sha256:20a302e03d4f10c912c249f7c26f8ecc06472dd2a2ad4abe91607bb7527de732
|
||||||
size 40996
|
size 25577
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:40c6334e01cc858f343f9046cfbb5079b79369002f5194965b6c2e75dd37b134
|
oid sha256:9939e7a38e0e280d42a996576a3cfbeb773625cf43ddf8afd04aad6ab35fd955
|
||||||
size 41437
|
size 53555
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:548950d915f6fbfdf1a17b81619d3d4cbefe93b79091d259bcec41ae6bbbc652
|
oid sha256:9939e7a38e0e280d42a996576a3cfbeb773625cf43ddf8afd04aad6ab35fd955
|
||||||
size 35422
|
size 53555
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6b47bcab08cded6e857fe213ab2c90602e557a36feebb45b2b8d4d18115ac68b
|
oid sha256:5547eb8b7f7ca596edf65bfd7703d1cb91fbf2e254374b9ea41d5c8754030e34
|
||||||
size 34582
|
size 41628
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:5cd5de47e285702bf3d2ee325fc83a1168a0367d9c6b54018565daa0ed503a77
|
oid sha256:adfd4213ac36d904a8a31b1c3bf5463ba72ad4760d084a4c412370d6d51cf250
|
||||||
size 36876
|
size 40996
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:59b6d7d5e484b34e1ce7814432144df34b1cd3323d31c90db439cbb5d9f8dc83
|
oid sha256:40c6334e01cc858f343f9046cfbb5079b79369002f5194965b6c2e75dd37b134
|
||||||
size 43588
|
size 41437
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:548950d915f6fbfdf1a17b81619d3d4cbefe93b79091d259bcec41ae6bbbc652
|
||||||
|
size 35422
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:6b47bcab08cded6e857fe213ab2c90602e557a36feebb45b2b8d4d18115ac68b
|
||||||
|
size 34582
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:5cd5de47e285702bf3d2ee325fc83a1168a0367d9c6b54018565daa0ed503a77
|
||||||
|
size 36876
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:59b6d7d5e484b34e1ce7814432144df34b1cd3323d31c90db439cbb5d9f8dc83
|
||||||
|
size 43588
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a3bebec99f2b870e7a38333590f5cb776b30f4f9b976d99601a7fb3bfd05a71f
|
oid sha256:093bb9672ecd80432c7e6102bd5ed2d1184cd384c91e479d99d13c0ba773bf78
|
||||||
size 40913
|
size 46448
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:853218e7ddc4d18b260f6ab047beb82778284851e230babc14113c6cb329d29a
|
oid sha256:ee80f7a765a453c3704af6d1f38f90637528bcf1e5966dc1bd016f4d34f21fa4
|
||||||
size 58504
|
size 61069
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e7caee4430244e294b75e3b6a5830088dd25397b1c272666eb2bec47ecd1f382
|
oid sha256:ee80f7a765a453c3704af6d1f38f90637528bcf1e5966dc1bd016f4d34f21fa4
|
||||||
size 57895
|
size 61069
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:bbb4224047c88d838de227e61332775e5efe0ce1380a7446c701f0db5b2d2dcc
|
oid sha256:853218e7ddc4d18b260f6ab047beb82778284851e230babc14113c6cb329d29a
|
||||||
size 58323
|
size 58504
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:8303e863849a00c16f00850854e8d4d7ceaf3fb097c7b20fe349127d8ed3b082
|
oid sha256:e7caee4430244e294b75e3b6a5830088dd25397b1c272666eb2bec47ecd1f382
|
||||||
size 20651
|
size 57895
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1626bffb649c204b0dcb1c92e7344d1b7f119b61ecac3c49840385f3a9a39515
|
oid sha256:bbb4224047c88d838de227e61332775e5efe0ce1380a7446c701f0db5b2d2dcc
|
||||||
size 40998
|
size 58323
|
||||||
|
|
|
||||||
|
|
@ -403,7 +403,8 @@
|
||||||
"name" : ":features:securityandprivacy:impl",
|
"name" : ":features:securityandprivacy:impl",
|
||||||
"includeRegex" : [
|
"includeRegex" : [
|
||||||
"screen\\.edit_room_address\\..*",
|
"screen\\.edit_room_address\\..*",
|
||||||
"screen\\.security_and_privacy\\..*"
|
"screen\\.security_and_privacy\\..*",
|
||||||
|
"screen\\.manage_authorized_spaces\\..*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue