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