feature(security&privacy): iterate on SpaceMember option
This commit is contained in:
@@ -12,12 +12,14 @@ import android.os.Parcelable
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.pop
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
@@ -31,12 +33,15 @@ import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.use
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
@@ -61,7 +66,7 @@ class SecurityAndPrivacyFlowNode(
|
||||
data object EditRoomAddress : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object ManageAuthorizedSpaces : NavTarget
|
||||
data class ManageAuthorizedSpaces(val initialSelection: List<RoomId>) : NavTarget
|
||||
}
|
||||
|
||||
private val callback: SecurityAndPrivacyEntryPoint.Callback = callback()
|
||||
@@ -83,6 +88,18 @@ class SecurityAndPrivacyFlowNode(
|
||||
callback.onDone()
|
||||
}
|
||||
}
|
||||
whenChildrenAttached { commonLifecycle: Lifecycle,
|
||||
securityAndPrivacyNode: SecurityAndPrivacyNode,
|
||||
manageAuthorizedSpacesNode: ManageAuthorizedSpacesNode ->
|
||||
commonLifecycle.coroutineScope.launch {
|
||||
val authorizedSpacesData = securityAndPrivacyNode.getAuthorizedSpacesData()
|
||||
val selectedSpaces = manageAuthorizedSpacesNode.waitForCompletion(authorizedSpacesData)
|
||||
withContext(NonCancellable) {
|
||||
backstack.pop()
|
||||
securityAndPrivacyNode.onAuthorizedSpacesSelected(selectedSpaces)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
@@ -93,7 +110,7 @@ class SecurityAndPrivacyFlowNode(
|
||||
NavTarget.EditRoomAddress -> {
|
||||
createNode<EditRoomAddressNode>(buildContext, plugins = listOf(navigator))
|
||||
}
|
||||
NavTarget.ManageAuthorizedSpaces -> {
|
||||
is NavTarget.ManageAuthorizedSpaces -> {
|
||||
createNode<ManageAuthorizedSpacesNode>(buildContext, plugins = listOf(navigator))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,12 +13,13 @@ import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.pop
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyEntryPoint
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
interface SecurityAndPrivacyNavigator : Plugin {
|
||||
fun onDone()
|
||||
fun openEditRoomAddress()
|
||||
fun closeEditRoomAddress()
|
||||
fun openManageAuthorizedSpaces()
|
||||
fun openManageAuthorizedSpaces(initialSelection: List<RoomId>)
|
||||
fun closeManageAuthorizedSpaces()
|
||||
}
|
||||
|
||||
@@ -38,8 +39,8 @@ class BackstackSecurityAndPrivacyNavigator(
|
||||
backStack.pop()
|
||||
}
|
||||
|
||||
override fun openManageAuthorizedSpaces() {
|
||||
backStack.push(SecurityAndPrivacyFlowNode.NavTarget.ManageAuthorizedSpaces)
|
||||
override fun openManageAuthorizedSpaces(initialSelection: List<RoomId>) {
|
||||
backStack.push(SecurityAndPrivacyFlowNode.NavTarget.ManageAuthorizedSpaces(initialSelection))
|
||||
}
|
||||
|
||||
override fun closeManageAuthorizedSpaces() {
|
||||
|
||||
@@ -11,6 +11,7 @@ package io.element.android.features.securityandprivacy.impl.manageauthorizedspac
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
sealed interface ManageAuthorizedSpacesEvent {
|
||||
data class SetData(val data: AuthorizedSpacesSelection) : ManageAuthorizedSpacesEvent
|
||||
data object Done : ManageAuthorizedSpacesEvent
|
||||
data class ToggleSpace(val roomId: RoomId) : ManageAuthorizedSpacesEvent
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
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
|
||||
@@ -18,7 +20,12 @@ import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.securityandprivacy.impl.SecurityAndPrivacyNavigator
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.appyx.launchMolecule
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
@AssistedInject
|
||||
@@ -27,12 +34,24 @@ class ManageAuthorizedSpacesNode(
|
||||
@Assisted plugins: List<Plugin>,
|
||||
presenterFactory: ManageAuthorizedSpacesPresenter.Factory,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
|
||||
data class Params(
|
||||
val initialSelection: List<RoomId>
|
||||
) : NodeInputs
|
||||
|
||||
private val navigator = plugins<SecurityAndPrivacyNavigator>().first()
|
||||
private val presenter = presenterFactory.create(navigator)
|
||||
|
||||
private val stateFlow = launchMolecule { presenter.present() }
|
||||
|
||||
suspend fun waitForCompletion(data: AuthorizedSpacesSelection): ImmutableList<RoomId> {
|
||||
stateFlow.value.eventSink(ManageAuthorizedSpacesEvent.SetData(data))
|
||||
return stateFlow.first { it.isSelectionComplete }.selectedIds
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
val state by stateFlow.collectAsState()
|
||||
ManageAuthorizedSpacesView(
|
||||
state = state,
|
||||
onBackClick = ::navigateUp,
|
||||
|
||||
@@ -9,16 +9,21 @@
|
||||
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.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.features.securityandprivacy.impl.SecurityAndPrivacyNavigator
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
@AssistedInject
|
||||
class ManageAuthorizedSpacesPresenter(
|
||||
@@ -33,19 +38,33 @@ class ManageAuthorizedSpacesPresenter(
|
||||
|
||||
@Composable
|
||||
override fun present(): ManageAuthorizedSpacesState {
|
||||
val roomInfo by room.roomInfoFlow.collectAsState()
|
||||
var currentSelection: ImmutableList<RoomId> by remember { mutableStateOf(persistentListOf()) }
|
||||
var spacesData by remember { mutableStateOf(AuthorizedSpacesSelection()) }
|
||||
var isSelectionComplete by remember { mutableStateOf(false) }
|
||||
|
||||
fun handleEvent(event: ManageAuthorizedSpacesEvent) {
|
||||
when (event) {
|
||||
ManageAuthorizedSpacesEvent.Done -> TODO()
|
||||
is ManageAuthorizedSpacesEvent.ToggleSpace -> TODO()
|
||||
ManageAuthorizedSpacesEvent.Done -> {
|
||||
isSelectionComplete = true
|
||||
}
|
||||
is ManageAuthorizedSpacesEvent.ToggleSpace -> {
|
||||
currentSelection = if (currentSelection.contains(event.roomId)) {
|
||||
currentSelection.minus(event.roomId).toPersistentList()
|
||||
} else {
|
||||
currentSelection.plus(event.roomId).toPersistentList()
|
||||
}
|
||||
}
|
||||
is ManageAuthorizedSpacesEvent.SetData -> {
|
||||
spacesData = event.data
|
||||
currentSelection = event.data.initialSelectedIds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ManageAuthorizedSpacesState(
|
||||
joinedSpaces = persistentListOf(),
|
||||
unknownSpaceIds = persistentListOf(),
|
||||
currentSelection = persistentListOf(),
|
||||
initialSelection = persistentListOf(),
|
||||
selection = spacesData,
|
||||
selectedIds = currentSelection,
|
||||
isSelectionComplete = isSelectionComplete,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,11 +11,17 @@ package io.element.android.features.securityandprivacy.impl.manageauthorizedspac
|
||||
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.persistentListOf
|
||||
|
||||
data class ManageAuthorizedSpacesState(
|
||||
val joinedSpaces: ImmutableList<SpaceRoom>,
|
||||
val unknownSpaceIds: ImmutableList<RoomId>,
|
||||
val currentSelection: ImmutableList<RoomId>,
|
||||
val initialSelection: ImmutableList<RoomId>,
|
||||
val selection: AuthorizedSpacesSelection,
|
||||
val selectedIds: ImmutableList<RoomId>,
|
||||
val isSelectionComplete: Boolean,
|
||||
val eventSink: (ManageAuthorizedSpacesEvent) -> Unit
|
||||
)
|
||||
|
||||
data class AuthorizedSpacesSelection(
|
||||
val joinedSpaces: ImmutableList<SpaceRoom> = persistentListOf(),
|
||||
val unknownSpaceIds: ImmutableList<RoomId> = persistentListOf(),
|
||||
val initialSelectedIds: ImmutableList<RoomId> = persistentListOf()
|
||||
)
|
||||
|
||||
@@ -20,11 +20,15 @@ open class ManageAuthorizedSpacesStateProvider : PreviewParameterProvider<Manage
|
||||
get() = sequenceOf(
|
||||
aManageAuthorizedSpacesState(),
|
||||
aManageAuthorizedSpacesState(
|
||||
unknownSpaceIds = listOf(aRoomId(99))
|
||||
authorizedSpacesData = anAuthorizedSpacesData(
|
||||
unknownSpaceIds = listOf(aRoomId(99))
|
||||
)
|
||||
),
|
||||
aManageAuthorizedSpacesState(
|
||||
currentSelection = listOf(aRoomId(1), aRoomId(3)),
|
||||
initialSelection = listOf(aRoomId(1)),
|
||||
authorizedSpacesData = anAuthorizedSpacesData(
|
||||
initialSelection = listOf(aRoomId(1)),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -45,17 +49,24 @@ private fun aSpaceRoomList(count: Int): List<SpaceRoom> {
|
||||
}
|
||||
}
|
||||
|
||||
private fun aManageAuthorizedSpacesState(
|
||||
fun anAuthorizedSpacesData(
|
||||
joinedSpaces: List<SpaceRoom> = aSpaceRoomList(5),
|
||||
unknownSpaceIds: List<RoomId> = emptyList(),
|
||||
currentSelection: List<RoomId> = emptyList(),
|
||||
initialSelection: List<RoomId> = emptyList(),
|
||||
eventSink: (ManageAuthorizedSpacesEvent) -> Unit = {},
|
||||
) = ManageAuthorizedSpacesState(
|
||||
) = AuthorizedSpacesSelection(
|
||||
joinedSpaces = joinedSpaces.toImmutableList(),
|
||||
unknownSpaceIds = unknownSpaceIds.toImmutableList(),
|
||||
currentSelection = currentSelection.toImmutableList(),
|
||||
initialSelection = initialSelection.toImmutableList(),
|
||||
initialSelectedIds = initialSelection.toImmutableList(),
|
||||
)
|
||||
|
||||
private fun aManageAuthorizedSpacesState(
|
||||
authorizedSpacesData: AuthorizedSpacesSelection = anAuthorizedSpacesData(),
|
||||
currentSelection: List<RoomId> = emptyList(),
|
||||
eventSink: (ManageAuthorizedSpacesEvent) -> Unit = {},
|
||||
) = ManageAuthorizedSpacesState(
|
||||
selection = authorizedSpacesData,
|
||||
selectedIds = currentSelection.toImmutableList(),
|
||||
isSelectionComplete = false,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
|
||||
@@ -66,12 +66,12 @@ fun ManageAuthorizedSpacesView(
|
||||
hasDivider = false,
|
||||
)
|
||||
}
|
||||
items(items = state.joinedSpaces) { space ->
|
||||
items(items = state.selection.joinedSpaces) { space ->
|
||||
CheckableSpaceListItem(
|
||||
headlineText = space.displayName,
|
||||
supportingText = space.canonicalAlias?.value,
|
||||
avatarData = space.getAvatarData(AvatarSize.SpaceMember),
|
||||
checked = state.currentSelection.contains(space.roomId),
|
||||
checked = state.selectedIds.contains(space.roomId),
|
||||
onCheckedChange = { _ ->
|
||||
state.eventSink(
|
||||
ManageAuthorizedSpacesEvent.ToggleSpace(space.roomId)
|
||||
@@ -79,19 +79,19 @@ fun ManageAuthorizedSpacesView(
|
||||
}
|
||||
)
|
||||
}
|
||||
if (state.unknownSpaceIds.isNotEmpty()) {
|
||||
if (state.selection.unknownSpaceIds.isNotEmpty()) {
|
||||
item {
|
||||
ListSectionHeader(
|
||||
title = stringResource(R.string.screen_manage_authorized_spaces_unknown_spaces_section_title),
|
||||
hasDivider = true,
|
||||
)
|
||||
}
|
||||
items(items = state.unknownSpaceIds) {
|
||||
items(items = state.selection.unknownSpaceIds) {
|
||||
CheckableSpaceListItem(
|
||||
headlineText = stringResource(R.string.screen_manage_authorized_spaces_unknown_space),
|
||||
supportingText = it.value,
|
||||
avatarData = null,
|
||||
checked = state.currentSelection.contains(it),
|
||||
checked = state.selectedIds.contains(it),
|
||||
onCheckedChange = { _ ->
|
||||
state.eventSink(
|
||||
ManageAuthorizedSpacesEvent.ToggleSpace(it)
|
||||
|
||||
@@ -23,9 +23,12 @@ import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.securityandprivacy.impl.SecurityAndPrivacyNavigator
|
||||
import io.element.android.features.securityandprivacy.impl.manageauthorizedspaces.AuthorizedSpacesSelection
|
||||
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
|
||||
import io.element.android.libraries.architecture.appyx.launchMolecule
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
@AssistedInject
|
||||
@@ -43,6 +46,16 @@ class SecurityAndPrivacyNode(
|
||||
activity.openUrlInChromeCustomTab(null, darkTheme, url)
|
||||
}
|
||||
|
||||
fun getAuthorizedSpacesData(): AuthorizedSpacesSelection{
|
||||
return stateFlow.value.getAuthorizedSpaceData()
|
||||
}
|
||||
|
||||
fun onAuthorizedSpacesSelected(selectedSpaces: ImmutableList<RoomId>) {
|
||||
stateFlow.value.eventSink(
|
||||
SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.SpaceMember(selectedSpaces))
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val activity = requireNotNull(LocalActivity.current)
|
||||
@@ -56,4 +69,5 @@ class SecurityAndPrivacyNode(
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
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.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomInfo
|
||||
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
|
||||
@@ -130,9 +131,9 @@ class SecurityAndPrivacyPresenter(
|
||||
value = (joinedParentSpaces + nonParentJoinedSpaces).toImmutableSet()
|
||||
}
|
||||
|
||||
val spaceSelection by remember {
|
||||
val spaceSelectionMode by remember {
|
||||
derivedStateOf {
|
||||
getSpaceSelection(selectableJoinedSpaces, savedSettings.roomAccess)
|
||||
getSpaceSelectionMode(selectableJoinedSpaces, savedSettings.roomAccess)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,9 +192,12 @@ class SecurityAndPrivacyPresenter(
|
||||
SecurityAndPrivacyEvent.DismissExitConfirmation -> {
|
||||
saveAction.value = AsyncAction.Uninitialized
|
||||
}
|
||||
SecurityAndPrivacyEvent.ManageAuthorizedSpaces -> navigator.openManageAuthorizedSpaces()
|
||||
SecurityAndPrivacyEvent.ManageAuthorizedSpaces -> {
|
||||
navigator.openManageAuthorizedSpaces(editedSettings.roomAccess.spaceIds())
|
||||
}
|
||||
SecurityAndPrivacyEvent.SelectSpaceMemberAccess -> handleSpaceMemberAccessSelection(
|
||||
spaceSelection = spaceSelection,
|
||||
spaceSelectionMode = spaceSelectionMode,
|
||||
spaceIds = editedSettings.roomAccess.spaceIds(),
|
||||
editedAccess = editedRoomAccess,
|
||||
)
|
||||
}
|
||||
@@ -216,7 +220,7 @@ class SecurityAndPrivacyPresenter(
|
||||
isSpace = roomInfo.isSpace,
|
||||
isSpaceSettingsEnabled = isSpaceSettingsEnabled,
|
||||
selectableJoinedSpaces = selectableJoinedSpaces,
|
||||
spaceSelection = spaceSelection,
|
||||
spaceSelectionMode = spaceSelectionMode,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
|
||||
@@ -241,42 +245,45 @@ class SecurityAndPrivacyPresenter(
|
||||
}
|
||||
|
||||
private fun handleSpaceMemberAccessSelection(
|
||||
spaceSelection: SpaceSelection,
|
||||
spaceSelectionMode: SpaceSelectionMode,
|
||||
spaceIds: List<RoomId>,
|
||||
editedAccess: MutableState<SecurityAndPrivacyRoomAccess>,
|
||||
) {
|
||||
if (editedAccess.value is SecurityAndPrivacyRoomAccess.SpaceMember) {
|
||||
return
|
||||
}
|
||||
when (spaceSelection) {
|
||||
is SpaceSelection.None -> Unit
|
||||
is SpaceSelection.Multiple -> navigator.openManageAuthorizedSpaces()
|
||||
is SpaceSelection.Single -> {
|
||||
when (spaceSelectionMode) {
|
||||
is SpaceSelectionMode.None -> Unit
|
||||
is SpaceSelectionMode.Multiple -> navigator.openManageAuthorizedSpaces(
|
||||
initialSelection = spaceIds ,
|
||||
)
|
||||
is SpaceSelectionMode.Single -> {
|
||||
val newRoomAccess = SecurityAndPrivacyRoomAccess.SpaceMember(
|
||||
spaceIds = persistentListOf(spaceSelection.spaceId)
|
||||
spaceIds = persistentListOf(spaceSelectionMode.spaceId)
|
||||
)
|
||||
editedAccess.value = newRoomAccess
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSpaceSelection(
|
||||
private fun getSpaceSelectionMode(
|
||||
selectableJoinedSpaces: Set<SpaceRoom>,
|
||||
savedAccess: SecurityAndPrivacyRoomAccess,
|
||||
): SpaceSelection {
|
||||
): SpaceSelectionMode {
|
||||
val selectableSpacesCount = (selectableJoinedSpaces.map { it.roomId } + savedAccess.spaceIds()).toSet().size
|
||||
return when {
|
||||
selectableSpacesCount == 0 -> SpaceSelection.None
|
||||
selectableSpacesCount > 1 -> SpaceSelection.Multiple
|
||||
selectableSpacesCount == 0 -> SpaceSelectionMode.None
|
||||
selectableSpacesCount > 1 -> SpaceSelectionMode.Multiple
|
||||
else -> {
|
||||
val joinedSpace = selectableJoinedSpaces.firstOrNull()
|
||||
if (joinedSpace != null) {
|
||||
SpaceSelection.Single(joinedSpace.roomId, joinedSpace)
|
||||
SpaceSelectionMode.Single(joinedSpace.roomId, joinedSpace)
|
||||
} else {
|
||||
val spaceId = savedAccess.spaceIds().firstOrNull()
|
||||
if (spaceId == null) {
|
||||
SpaceSelection.None
|
||||
SpaceSelectionMode.None
|
||||
} else {
|
||||
SpaceSelection.Single(spaceId, null)
|
||||
SpaceSelectionMode.Single(spaceId, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,11 @@
|
||||
|
||||
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.features.securityandprivacy.impl.manageauthorizedspaces.AuthorizedSpacesSelection
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
@@ -25,16 +29,32 @@ data class SecurityAndPrivacyState(
|
||||
val editedSettings: SecurityAndPrivacySettings,
|
||||
val homeserverName: String,
|
||||
val showEnableEncryptionConfirmation: Boolean,
|
||||
val isKnockEnabled: Boolean,
|
||||
val isSpaceSettingsEnabled: 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 spaceSelection: SpaceSelection,
|
||||
private val spaceSelectionMode: SpaceSelectionMode,
|
||||
val eventSink: (SecurityAndPrivacyEvent) -> Unit
|
||||
) {
|
||||
|
||||
val isSpaceMemberSelectable = isSpaceSettingsEnabled && spaceSelectionMode != SpaceSelectionMode.None
|
||||
|
||||
// Show SpaceMember option in two cases:
|
||||
// - the SpaceSettings FF is enabled
|
||||
// - SpaceMember is the current saved value
|
||||
val showSpaceMemberOption = savedSettings.roomAccess is SecurityAndPrivacyRoomAccess.SpaceMember || isSpaceMemberSelectable
|
||||
|
||||
val showManageSpaceAction = spaceSelectionMode is SpaceSelectionMode.Multiple && editedSettings.roomAccess is SecurityAndPrivacyRoomAccess.SpaceMember
|
||||
|
||||
val isAskToJoinSelectable = isKnockEnabled
|
||||
|
||||
// Show Ask to join option in two cases:
|
||||
// - the Knock FF is enabled
|
||||
// - AskToJoin is the current saved value
|
||||
val showAskToJoinOption = savedSettings.roomAccess == SecurityAndPrivacyRoomAccess.AskToJoin || isAskToJoinSelectable
|
||||
|
||||
val canBeSaved = savedSettings != editedSettings
|
||||
|
||||
// Logic is in https://github.com/element-hq/element-meta/issues/3029
|
||||
@@ -57,6 +77,32 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
fun getAuthorizedSpaceData(): AuthorizedSpacesSelection {
|
||||
return AuthorizedSpacesSelection(
|
||||
joinedSpaces = selectableJoinedSpaces.toImmutableList(),
|
||||
unknownSpaceIds = savedSettings.roomAccess.spaceIds().filter { spaceId ->
|
||||
selectableJoinedSpaces.none { it.roomId == spaceId }
|
||||
}.toImmutableList(),
|
||||
initialSelectedIds = editedSettings.roomAccess.spaceIds().toImmutableList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class SecurityAndPrivacySettings(
|
||||
@@ -85,10 +131,10 @@ enum class SecurityAndPrivacyHistoryVisibility {
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface SpaceSelection {
|
||||
data object None : SpaceSelection
|
||||
data class Single(val spaceId: RoomId, val spaceRoom: SpaceRoom?) : SpaceSelection
|
||||
data object Multiple : SpaceSelection
|
||||
sealed interface SpaceSelectionMode {
|
||||
data object None : SpaceSelectionMode
|
||||
data class Single(val spaceId: RoomId, val spaceRoom: SpaceRoom?) : SpaceSelectionMode
|
||||
data object Multiple : SpaceSelectionMode
|
||||
}
|
||||
|
||||
sealed interface SecurityAndPrivacyRoomAccess {
|
||||
|
||||
@@ -14,7 +14,6 @@ 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.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
|
||||
open class SecurityAndPrivacyStateProvider : PreviewParameterProvider<SecurityAndPrivacyState> {
|
||||
@@ -122,7 +121,7 @@ fun aSecurityAndPrivacyState(
|
||||
isKnockEnabled: Boolean = true,
|
||||
isSpace: Boolean = false,
|
||||
selectableJoinedSpaces: Set<SpaceRoom> = emptySet(),
|
||||
spaceSelection: SpaceSelection = SpaceSelection.None,
|
||||
spaceSelectionMode: SpaceSelectionMode = SpaceSelectionMode.None,
|
||||
isSpaceSettingsEnabled: Boolean = true,
|
||||
eventSink: (SecurityAndPrivacyEvent) -> Unit = {}
|
||||
) = SecurityAndPrivacyState(
|
||||
@@ -135,7 +134,7 @@ fun aSecurityAndPrivacyState(
|
||||
permissions = permissions,
|
||||
isSpace = isSpace,
|
||||
selectableJoinedSpaces = selectableJoinedSpaces.toImmutableSet(),
|
||||
spaceSelection = SpaceSelection.None,
|
||||
spaceSelectionMode = spaceSelectionMode,
|
||||
isSpaceSettingsEnabled = isSpaceSettingsEnabled,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
@@ -90,14 +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,
|
||||
isSpaceSettingsEnabled = state.isSpaceSettingsEnabled,
|
||||
onSelectOption = { state.eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(it)) },
|
||||
onManageSpacesClick = { state.eventSink(SecurityAndPrivacyEvent.ManageAuthorizedSpaces) },
|
||||
onSpaceMemberAccessClick = { state.eventSink(SecurityAndPrivacyEvent.SelectSpaceMemberAccess) }
|
||||
)
|
||||
}
|
||||
if (state.showRoomVisibilitySections) {
|
||||
@@ -211,15 +205,25 @@ private fun SecurityAndPrivacySection(
|
||||
|
||||
@Composable
|
||||
private fun RoomAccessSection(
|
||||
edited: SecurityAndPrivacyRoomAccess,
|
||||
saved: SecurityAndPrivacyRoomAccess,
|
||||
isKnockEnabled: Boolean,
|
||||
isSpaceSettingsEnabled: Boolean,
|
||||
onSelectOption: (SecurityAndPrivacyRoomAccess) -> Unit,
|
||||
onSpaceMemberAccessClick: () -> Unit,
|
||||
onManageSpacesClick: () -> Unit,
|
||||
state: SecurityAndPrivacyState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
||||
val edited = state.editedSettings.roomAccess
|
||||
val saved = state.savedSettings.roomAccess
|
||||
|
||||
fun onSelectOption(option: SecurityAndPrivacyRoomAccess) {
|
||||
state.eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(option))
|
||||
}
|
||||
|
||||
fun onSpaceMemberAccessClick() {
|
||||
state.eventSink(SecurityAndPrivacyEvent.SelectSpaceMemberAccess)
|
||||
}
|
||||
|
||||
fun onManageSpacesClick() {
|
||||
state.eventSink(SecurityAndPrivacyEvent.ManageAuthorizedSpaces)
|
||||
}
|
||||
|
||||
SecurityAndPrivacySection(
|
||||
title = stringResource(R.string.screen_security_and_privacy_room_access_section_header),
|
||||
modifier = modifier,
|
||||
@@ -231,31 +235,25 @@ private fun RoomAccessSection(
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Public())),
|
||||
onClick = { onSelectOption(SecurityAndPrivacyRoomAccess.Anyone) },
|
||||
)
|
||||
// Show SpaceMember option in two cases:
|
||||
// - the SpaceSettings FF is enabled
|
||||
// - SpaceMember is the current saved value
|
||||
if (saved is SecurityAndPrivacyRoomAccess.SpaceMember || isSpaceSettingsEnabled)
|
||||
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 is SecurityAndPrivacyRoomAccess.SpaceMember),
|
||||
trailingContent = ListItemContent.RadioButton(selected = state.editedSettings.roomAccess is SecurityAndPrivacyRoomAccess.SpaceMember),
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Space())),
|
||||
onClick = onSpaceMemberAccessClick,
|
||||
enabled = isSpaceSettingsEnabled,
|
||||
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,
|
||||
)
|
||||
}
|
||||
ListItem(
|
||||
@@ -265,11 +263,11 @@ private fun RoomAccessSection(
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Lock())),
|
||||
onClick = { onSelectOption(SecurityAndPrivacyRoomAccess.InviteOnly) },
|
||||
)
|
||||
if (edited is SecurityAndPrivacyRoomAccess.SpaceMember) {
|
||||
if (state.showManageSpaceAction) {
|
||||
val footerText = stringWithLink(
|
||||
textRes = R.string.screen_security_and_privacy_room_access_footer,
|
||||
url = stringResource(R.string.screen_security_and_privacy_room_access_footer_manage_spaces_action),
|
||||
onLinkClick = { onManageSpacesClick() },
|
||||
onLinkClick = {onManageSpacesClick()},
|
||||
)
|
||||
Text(
|
||||
text = footerText,
|
||||
|
||||
Reference in New Issue
Block a user