From 7b8950a51bc968f96ad7c0cfbd9700a1a994a114 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 18 Dec 2025 10:07:57 +0100 Subject: [PATCH] feature(security&privacy): start ManageAuthorizedSpacesView --- .../ManageAuthorizedSpacesPresenter.kt | 6 +- .../ManageAuthorizedSpacesState.kt | 8 ++ .../ManageAuthorizedSpacesStateProvider.kt | 48 +++++++- .../ManageAuthorizedSpacesView.kt | 115 +++++++++++++++++- 4 files changed, 171 insertions(+), 6 deletions(-) diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt index 758ea030d2..527393c9d3 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesPresenter.kt @@ -18,6 +18,7 @@ import io.element.android.features.securityandprivacy.impl.SecurityAndPrivacyNav import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.room.JoinedRoom +import kotlinx.collections.immutable.persistentListOf @AssistedInject class EditRoomAddressPresenter( @@ -33,7 +34,6 @@ class EditRoomAddressPresenter( @Composable override fun present(): ManageAuthorizedSpacesState { val roomInfo by room.roomInfoFlow.collectAsState() - fun handleEvent(event: ManageAuthorizedSpacesEvent) { when (event) { ManageAuthorizedSpacesEvent.Done -> TODO() @@ -42,6 +42,10 @@ class EditRoomAddressPresenter( } return ManageAuthorizedSpacesState( + joinedSpaces = persistentListOf(), + unknownSpaceIds = persistentListOf(), + currentSelection = persistentListOf(), + initialSelection = persistentListOf(), eventSink = ::handleEvent, ) } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesState.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesState.kt index f0936e81b7..688ef6e6e9 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesState.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesState.kt @@ -8,6 +8,14 @@ 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 + data class ManageAuthorizedSpacesState( + val joinedSpaces: ImmutableList, + val unknownSpaceIds: ImmutableList, + val currentSelection: ImmutableList, + val initialSelection: ImmutableList, val eventSink: (ManageAuthorizedSpacesEvent) -> Unit ) diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt index 91ff1f9b22..d91e16f3e6 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt @@ -9,11 +9,53 @@ package io.element.android.features.securityandprivacy.impl.manageauthorizedspaces import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity +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 open class ManageAuthorizedSpacesStateProvider : PreviewParameterProvider { override val values: Sequence - get() = sequenceOf() + get() = sequenceOf( + aManageAuthorizedSpacesState(), + aManageAuthorizedSpacesState( + unknownSpaceIds = listOf(aRoomId(99)) + ), + aManageAuthorizedSpacesState( + currentSelection = listOf(aRoomId(1), aRoomId(3)), + initialSelection = listOf(aRoomId(1)), + ), + ) } +private fun aRoomId(index: Int) = RoomId("!roomId$index:matrix.org") + +private fun aSpaceRoomList(count: Int): List { + return (1..count).map { index -> + aSpaceRoom( + roomId = aRoomId(index), + displayName = "Space $index", + canonicalAlias = if (index % 2 == 0) { + RoomAlias("#space$index:matrix.org") + } else { + null + } + ) + } +} + +private fun aManageAuthorizedSpacesState( + joinedSpaces: List = aSpaceRoomList(5), + unknownSpaceIds: List = emptyList(), + currentSelection: List = emptyList(), + initialSelection: List = emptyList(), + eventSink: (ManageAuthorizedSpacesEvent) -> Unit = {}, +) = ManageAuthorizedSpacesState( + joinedSpaces = joinedSpaces.toImmutableList(), + unknownSpaceIds = unknownSpaceIds.toImmutableList(), + currentSelection = currentSelection.toImmutableList(), + initialSelection = initialSelection.toImmutableList(), + eventSink = eventSink, +) + diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt index 632fb7ffcc..ed49e048aa 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt @@ -10,17 +10,34 @@ package io.element.android.features.securityandprivacy.impl.manageauthorizedspac 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.api.spaces.SpaceRoom +import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.ui.strings.CommonStrings @Composable @@ -43,11 +60,105 @@ fun ManageAuthorizedSpacesView( 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.joinedSpaces) { space -> + CheckableSpaceListItem( + headlineText = space.displayName, + supportingText = space.canonicalAlias?.value, + avatarData = space.getAvatarData(AvatarSize.SpaceMember), + checked = state.currentSelection.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 = false, + ) + } + items(items = state.unknownSpaceIds) { + CheckableSpaceListItem( + headlineText = stringResource(R.string.screen_manage_authorized_spaces_unknown_space), + supportingText = it.value, + avatarData = null, + checked = state.currentSelection.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( @@ -57,7 +168,7 @@ private fun ManageAuthorizedSpacesTopBar( ) { TopAppBar( modifier = modifier, - titleStr = stringResource(CommonStrings.screen_manage_authorized_spaces_title), + titleStr = stringResource(R.string.screen_manage_authorized_spaces_title), navigationIcon = { BackButton(onClick = onBackClick) }, actions = { TextButton(