Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
485585d
feature(security&privacy): create ManageAuthorizedSpace classes
ganfra Dec 3, 2025
7645588
localazy : sync strings
ganfra Dec 17, 2025
161733b
feature(security&privacy): start ManageAuthorizedSpacesView
ganfra Dec 18, 2025
4a0e654
localazy: sync strings
ganfra Dec 23, 2025
c398c62
space service : expose methods from sdk
ganfra Dec 23, 2025
f64cb6e
feature(security&privacy): start branching logic of ManageAuthorizedS…
ganfra Dec 30, 2025
6a4ab9b
quality: rename class
ganfra Jan 5, 2026
cdc3cdc
feature(security&privacy): make spaceSelection part of the state
ganfra Jan 5, 2026
887e59b
feature(security&privacy): check SpaceSettings ff
ganfra Jan 5, 2026
4e9d5c5
feature(security&privacy): iterate on SpaceMember option
ganfra Jan 6, 2026
0d11c43
feature(security&privacy): working SpaceMember selection
ganfra Jan 7, 2026
8da3a3a
feature(security&privacy): support KnockRestricted join rule
ganfra Jan 7, 2026
b3463a5
quality: add bunch of tests for Security&Privacy new features
ganfra Jan 7, 2026
6eaf608
Fix SecurityAndPrivacy preview state configuration
ganfra Jan 7, 2026
b3a934b
quality : format and clean
ganfra Jan 7, 2026
0668135
Merge branch 'develop' into feature/fga/space_members_access
ganfra Jan 8, 2026
bf7afd5
Update screenshots
ElementBot Jan 8, 2026
c02a61e
Add comprehensive presenter tests for SecurityAndPrivacy feature
ganfra Jan 8, 2026
ea7e8e7
Fix SecurityAndPrivacy "manage spaces" footer text
ganfra Jan 8, 2026
184e0fb
quality: move tests to matching package
ganfra Jan 8, 2026
29f50d0
Update screenshots
ElementBot Jan 8, 2026
9a30e4d
Refactor space selection to use SpaceSelectionStateHolder
ganfra Jan 9, 2026
75c73be
quality : fix PR remarks
ganfra Jan 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -89,6 +96,9 @@ class SecurityAndPrivacyFlowNode(
NavTarget.EditRoomAddress -> {
createNode<EditRoomAddressNode>(buildContext, plugins = listOf(navigator))
}
NavTarget.ManageAuthorizedSpaces -> {
createNode<ManageAuthorizedSpacesNode>(buildContext, plugins = listOf(navigator))
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ interface SecurityAndPrivacyNavigator : Plugin {
fun onDone()
fun openEditRoomAddress()
fun closeEditRoomAddress()
fun openManageAuthorizedSpaces()
fun closeManageAuthorizedSpaces()
}

class BackstackSecurityAndPrivacyNavigator(
Expand All @@ -35,4 +37,12 @@ class BackstackSecurityAndPrivacyNavigator(
override fun closeEditRoomAddress() {
backStack.pop()
}

override fun openManageAuthorizedSpaces() {
backStack.push(SecurityAndPrivacyFlowNode.NavTarget.ManageAuthorizedSpaces)
}

override fun closeManageAuthorizedSpaces() {
backStack.pop()
}
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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
)
}
}
Original file line number Diff line number Diff line change
@@ -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,
)
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
@@ -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,
)
Loading
Loading