From a89484e8261aa2874b1154f8f217d6dcc0c21a84 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 28 Nov 2025 14:20:52 +0100 Subject: [PATCH 1/6] space: mark space ff as finished --- .../io/element/android/libraries/featureflag/api/FeatureFlags.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt index d809fe34dc..997d4c2684 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt @@ -74,6 +74,7 @@ enum class FeatureFlags( key = "feature.space", title = "Spaces", defaultValue = { true }, + isFinished = true, isFinished = false, ), PrintLogsToLogcat( From 185ef152fe51e80faf86ffbc804ae408155c065b Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 28 Nov 2025 14:50:55 +0100 Subject: [PATCH 2/6] space: add space settings ff and branch it --- .../android/features/space/impl/root/SpacePresenter.kt | 8 ++++++++ .../android/features/space/impl/root/SpaceState.kt | 1 + .../features/space/impl/root/SpaceStateProvider.kt | 2 ++ .../element/android/features/space/impl/root/SpaceView.kt | 5 +++-- .../android/libraries/featureflag/api/FeatureFlags.kt | 6 ++++++ 5 files changed, 20 insertions(+), 2 deletions(-) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt index ce76aa457f..3d36de0cea 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt @@ -26,6 +26,8 @@ import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.mapState import io.element.android.libraries.di.annotations.SessionCoroutineScope +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.RoomId import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias @@ -53,6 +55,7 @@ class SpacePresenter( private val joinRoom: JoinRoom, private val acceptDeclineInvitePresenter: Presenter, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, + private val featureFlagService: FeatureFlagService, ) : Presenter { private var children by mutableStateOf>(persistentListOf()) @@ -79,6 +82,10 @@ class SpacePresenter( } }.collectAsState() + val isSpaceSettingsEnabled by remember { + featureFlagService.isFeatureEnabledFlow(FeatureFlags.SpaceSettings) + }.collectAsState(false) + val currentSpace by spaceRoomList.currentSpaceFlow.collectAsState() val (joinActions, setJoinActions) = remember { mutableStateOf(emptyMap>()) } @@ -129,6 +136,7 @@ class SpacePresenter( joinActions = joinActions.toImmutableMap(), acceptDeclineInviteState = acceptDeclineInviteState, topicViewerState = topicViewerState, + canAccessSpaceSettings = isSpaceSettingsEnabled, eventSink = ::handleEvent, ) } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt index 031721ee26..cceda62806 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt @@ -26,6 +26,7 @@ data class SpaceState( val joinActions: ImmutableMap>, val acceptDeclineInviteState: AcceptDeclineInviteState, val topicViewerState: TopicViewerState, + val canAccessSpaceSettings: Boolean, val eventSink: (SpaceEvents) -> Unit ) { fun isJoining(spaceId: RoomId): Boolean = joinActions[spaceId] == AsyncAction.Loading diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt index bfb63c63db..52894ad599 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt @@ -53,6 +53,7 @@ fun aSpaceState( hasMoreToLoad: Boolean = true, acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(), topicViewerState: TopicViewerState = TopicViewerState.Hidden, + canAccessSpaceSettings: Boolean = true, eventSink: (SpaceEvents) -> Unit = { }, ) = SpaceState( currentSpace = parentSpace, @@ -63,6 +64,7 @@ fun aSpaceState( joinActions = joinActions.toImmutableMap(), acceptDeclineInviteState = acceptDeclineInviteState, topicViewerState = topicViewerState, + canAccessSpaceSettings = canAccessSpaceSettings, eventSink = eventSink, ) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt index cd1b3de74d..862b9882d2 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt @@ -88,6 +88,7 @@ fun SpaceView( topBar = { SpaceViewTopBar( currentSpace = state.currentSpace, + canAccessSpaceSettings = state.canAccessSpaceSettings, onBackClick = onBackClick, onLeaveSpaceClick = onLeaveSpaceClick, onShareSpace = onShareSpace, @@ -255,6 +256,7 @@ private fun LoadingMoreIndicator( @Composable private fun SpaceViewTopBar( currentSpace: SpaceRoom?, + canAccessSpaceSettings: Boolean, onBackClick: () -> Unit, onLeaveSpaceClick: () -> Unit, onDetailsClick: () -> Unit, @@ -275,8 +277,7 @@ private fun SpaceViewTopBar( avatarData = currentSpace.getAvatarData(AvatarSize.TimelineRoom), modifier = Modifier .clip(roundedCornerShape) - // TODO enable when screen ready for space - .clickable(enabled = false, onClick = onDetailsClick) + .clickable(enabled = canAccessSpaceSettings, onClick = onDetailsClick) ) } }, diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt index 997d4c2684..2b8eb0f397 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt @@ -75,6 +75,12 @@ enum class FeatureFlags( title = "Spaces", defaultValue = { true }, isFinished = true, + ), + SpaceSettings( + key = "feature.spaceSettings", + title = "Space settings", + description = "Allow managing space settings such details, permissions and privacy.", + defaultValue = { true }, isFinished = false, ), PrintLogsToLogcat( From 86a2f55db317c74c915522c3c4253a6e3d949c1a Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 28 Nov 2025 15:54:05 +0100 Subject: [PATCH 3/6] space: start branching space settings flow --- features/space/impl/build.gradle.kts | 2 + .../features/space/impl/SpaceFlowNode.kt | 26 ++-- .../impl/settings/SpaceSettingsFlowNode.kt | 116 ++++++++++++++++++ .../space/impl/settings/SpaceSettingsNode.kt | 4 +- 4 files changed, 127 insertions(+), 21 deletions(-) create mode 100644 features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsFlowNode.kt diff --git a/features/space/impl/build.gradle.kts b/features/space/impl/build.gradle.kts index d212bac36b..4ee6822bc5 100644 --- a/features/space/impl/build.gradle.kts +++ b/features/space/impl/build.gradle.kts @@ -40,6 +40,8 @@ dependencies { implementation(projects.libraries.featureflag.api) implementation(projects.features.invite.api) implementation(projects.libraries.previewutils) + implementation(projects.features.securityandprivacy.api) + implementation(projects.features.rolesandpermissions.api) api(projects.features.space.api) testCommonDependencies(libs, true) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt index 036eab1c22..5fe646aaeb 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt @@ -28,7 +28,7 @@ import io.element.android.features.space.api.SpaceEntryPoint import io.element.android.features.space.impl.di.SpaceFlowGraph import io.element.android.features.space.impl.leave.LeaveSpaceNode import io.element.android.features.space.impl.root.SpaceNode -import io.element.android.features.space.impl.settings.SpaceSettingsNode +import io.element.android.features.space.impl.settings.SpaceSettingsFlowNode import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.callback @@ -115,32 +115,20 @@ class SpaceFlowNode( createNode(buildContext, listOf(callback)) } NavTarget.Settings -> { - val callback = object : SpaceSettingsNode.Callback { - override fun closeSettings() { - backstack.pop() - } - - override fun navigateToSpaceInfo() { - // TODO - } - + val callback = object : SpaceSettingsFlowNode.Callback { override fun navigateToSpaceMembers() { callback.navigateToRoomMemberList() } - override fun navigateToRolesAndPermissions() { - // TODO - } - - override fun navigateToSecurityAndPrivacy() { - // TODO - } - override fun startLeaveSpaceFlow() { backstack.push(NavTarget.Leave) } + + override fun closeSettings() { + backstack.pop() + } } - createNode(buildContext, listOf(callback)) + createNode(buildContext, listOf(callback)) } } } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsFlowNode.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsFlowNode.kt new file mode 100644 index 0000000000..b52da111b3 --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsFlowNode.kt @@ -0,0 +1,116 @@ +/* + * 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.space.impl.settings + +import android.os.Parcelable +import androidx.compose.runtime.Composable +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.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.push +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode +import io.element.android.features.rolesandpermissions.api.RolesAndPermissionsEntryPoint +import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyEntryPoint +import io.element.android.features.space.impl.di.SpaceFlowScope +import io.element.android.libraries.architecture.BackstackView +import io.element.android.libraries.architecture.BaseFlowNode +import io.element.android.libraries.architecture.callback +import io.element.android.libraries.architecture.createNode +import kotlinx.parcelize.Parcelize + +@ContributesNode(SpaceFlowScope::class) +@AssistedInject +class SpaceSettingsFlowNode( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val securityAndPrivacyEntryPoint: SecurityAndPrivacyEntryPoint, + private val rolesAndPermissionsEntryPoint: RolesAndPermissionsEntryPoint, +) : BaseFlowNode( + backstack = BackStack( + initialElement = NavTarget.Root, + savedStateMap = buildContext.savedStateMap, + ), + buildContext = buildContext, + plugins = plugins, +) { + interface Callback : Plugin { + fun navigateToSpaceMembers() + fun startLeaveSpaceFlow() + fun closeSettings() + } + + sealed interface NavTarget : Parcelable { + @Parcelize + data object Root : NavTarget + + @Parcelize + data object SecurityAndPrivacy : NavTarget + + @Parcelize + data object RolesAndPermissions : NavTarget + } + + private val callback: Callback = callback() + + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { + return when (navTarget) { + is NavTarget.Root -> { + val callback = object : SpaceSettingsNode.Callback { + override fun closeSettings() { + callback.closeSettings() + } + + override fun navigateToEditDetails() { + // TODO + } + + override fun navigateToSpaceMembers() { + callback.navigateToSpaceMembers() + } + + override fun navigateToRolesAndPermissions() { + backstack.push(NavTarget.RolesAndPermissions) + } + + override fun navigateToSecurityAndPrivacy() { + backstack.push(NavTarget.SecurityAndPrivacy) + } + + override fun startLeaveSpaceFlow() { + callback.startLeaveSpaceFlow() + } + } + createNode( + buildContext = buildContext, + plugins = listOf(callback), + ) + } + is NavTarget.SecurityAndPrivacy -> { + securityAndPrivacyEntryPoint.createNode( + parentNode = this, + buildContext = buildContext, + ) + } + is NavTarget.RolesAndPermissions -> { + rolesAndPermissionsEntryPoint.createNode( + parentNode = this, + buildContext = buildContext, + ) + } + } + } + + @Composable + override fun View(modifier: Modifier) { + BackstackView(modifier) + } +} diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsNode.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsNode.kt index ae2f4857c0..b1e64fbba1 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsNode.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsNode.kt @@ -32,7 +32,7 @@ class SpaceSettingsNode( interface Callback : Plugin { fun closeSettings() - fun navigateToSpaceInfo() + fun navigateToEditDetails() fun navigateToSpaceMembers() fun navigateToRolesAndPermissions() fun navigateToSecurityAndPrivacy() @@ -48,7 +48,7 @@ class SpaceSettingsNode( SpaceSettingsView( state = state, modifier = modifier, - onSpaceInfoClick = callback::navigateToSpaceInfo, + onSpaceInfoClick = callback::navigateToEditDetails, onBackClick = callback::closeSettings, onMembersClick = callback::navigateToSpaceMembers, onRolesAndPermissionsClick = callback::navigateToRolesAndPermissions, From 5f2134b2d5c7c5f2be33d68faf3578da2e2b09cf Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 3 Dec 2025 11:14:20 +0100 Subject: [PATCH 4/6] space: SpaceSettings feature flag should be off by default --- .../element/android/libraries/featureflag/api/FeatureFlags.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt index 2b8eb0f397..9bacb3c0d6 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt @@ -80,7 +80,7 @@ enum class FeatureFlags( key = "feature.spaceSettings", title = "Space settings", description = "Allow managing space settings such details, permissions and privacy.", - defaultValue = { true }, + defaultValue = { false }, isFinished = false, ), PrintLogsToLogcat( From 3633866ace2deacd84d8e4ed178ade015fab3f01 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 3 Dec 2025 11:28:24 +0100 Subject: [PATCH 5/6] quality: fix SpacePresenterTest --- .../space/impl/root/SpacePresenterTest.kt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt index 51e3634345..9b6df0d258 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt @@ -19,6 +19,8 @@ import io.element.android.features.invite.api.toInviteData import io.element.android.features.invite.test.InMemorySeenInvitesStore import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomIdOrAlias import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias @@ -62,11 +64,22 @@ class SpacePresenterTest { assertThat(state.joinActions).isEmpty() assertThat(state.acceptDeclineInviteState).isEqualTo(anAcceptDeclineInviteState()) assertThat(state.topicViewerState).isEqualTo(TopicViewerState.Hidden) + assertThat(state.canAccessSpaceSettings).isFalse() advanceUntilIdle() paginateResult.assertions().isCalledOnce() } } + @Test + fun `present - canAccessSpaceSettings when space settings ff is enabled`() = runTest { + val presenter = createSpacePresenter(spaceSettingsEnabled = true) + presenter.test { + skipItems(1) + val state = awaitItem() + assertThat(state.canAccessSpaceSettings).isTrue() + } + } + @Test fun `present - load more`() = runTest { val paginateResult = lambdaRecorder> { @@ -328,6 +341,7 @@ class SpacePresenterTest { lambda = { _, _, _ -> Result.success(Unit) }, ), acceptDeclineInvitePresenter: Presenter = Presenter { anAcceptDeclineInviteState() }, + spaceSettingsEnabled: Boolean = false, ): SpacePresenter { return SpacePresenter( client = client, @@ -336,6 +350,11 @@ class SpacePresenterTest { joinRoom = joinRoom, acceptDeclineInvitePresenter = acceptDeclineInvitePresenter, sessionCoroutineScope = backgroundScope, + featureFlagService = FakeFeatureFlagService( + initialState = mapOf( + FeatureFlags.SpaceSettings.key to spaceSettingsEnabled, + ) + ), ) } } From d4a0559f395a031102f9d9c7fc3643b53e4e0197 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 3 Dec 2025 17:20:56 +0100 Subject: [PATCH 6/6] change : update wording of SpaceSettings ff Co-authored-by: Benoit Marty --- .../element/android/libraries/featureflag/api/FeatureFlags.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt index 9bacb3c0d6..2cbfb50944 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt @@ -79,7 +79,7 @@ enum class FeatureFlags( SpaceSettings( key = "feature.spaceSettings", title = "Space settings", - description = "Allow managing space settings such details, permissions and privacy.", + description = "Allow managing space settings such as details, permissions and privacy.", defaultValue = { false }, isFinished = false, ),