Skip to content

Commit e9e699e

Browse files
authored
Merge pull request #5827 from element-hq/feature/fga/space_feature_flags
Space feature flags
2 parents c6095bb + d4a0559 commit e9e699e

File tree

10 files changed

+167
-23
lines changed

10 files changed

+167
-23
lines changed

features/space/impl/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ dependencies {
4040
implementation(projects.libraries.featureflag.api)
4141
implementation(projects.features.invite.api)
4242
implementation(projects.libraries.previewutils)
43+
implementation(projects.features.securityandprivacy.api)
44+
implementation(projects.features.rolesandpermissions.api)
4345
api(projects.features.space.api)
4446

4547
testCommonDependencies(libs, true)

features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import io.element.android.features.space.api.SpaceEntryPoint
2828
import io.element.android.features.space.impl.di.SpaceFlowGraph
2929
import io.element.android.features.space.impl.leave.LeaveSpaceNode
3030
import io.element.android.features.space.impl.root.SpaceNode
31-
import io.element.android.features.space.impl.settings.SpaceSettingsNode
31+
import io.element.android.features.space.impl.settings.SpaceSettingsFlowNode
3232
import io.element.android.libraries.architecture.BackstackView
3333
import io.element.android.libraries.architecture.BaseFlowNode
3434
import io.element.android.libraries.architecture.callback
@@ -115,32 +115,20 @@ class SpaceFlowNode(
115115
createNode<SpaceNode>(buildContext, listOf(callback))
116116
}
117117
NavTarget.Settings -> {
118-
val callback = object : SpaceSettingsNode.Callback {
119-
override fun closeSettings() {
120-
backstack.pop()
121-
}
122-
123-
override fun navigateToSpaceInfo() {
124-
// TODO
125-
}
126-
118+
val callback = object : SpaceSettingsFlowNode.Callback {
127119
override fun navigateToSpaceMembers() {
128120
callback.navigateToRoomMemberList()
129121
}
130122

131-
override fun navigateToRolesAndPermissions() {
132-
// TODO
133-
}
134-
135-
override fun navigateToSecurityAndPrivacy() {
136-
// TODO
137-
}
138-
139123
override fun startLeaveSpaceFlow() {
140124
backstack.push(NavTarget.Leave)
141125
}
126+
127+
override fun closeSettings() {
128+
backstack.pop()
129+
}
142130
}
143-
createNode<SpaceSettingsNode>(buildContext, listOf(callback))
131+
createNode<SpaceSettingsFlowNode>(buildContext, listOf(callback))
144132
}
145133
}
146134
}

features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import io.element.android.libraries.architecture.AsyncAction
2626
import io.element.android.libraries.architecture.Presenter
2727
import io.element.android.libraries.core.coroutine.mapState
2828
import io.element.android.libraries.di.annotations.SessionCoroutineScope
29+
import io.element.android.libraries.featureflag.api.FeatureFlagService
30+
import io.element.android.libraries.featureflag.api.FeatureFlags
2931
import io.element.android.libraries.matrix.api.MatrixClient
3032
import io.element.android.libraries.matrix.api.core.RoomId
3133
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
@@ -53,6 +55,7 @@ class SpacePresenter(
5355
private val joinRoom: JoinRoom,
5456
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
5557
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
58+
private val featureFlagService: FeatureFlagService,
5659
) : Presenter<SpaceState> {
5760
private var children by mutableStateOf<ImmutableList<SpaceRoom>>(persistentListOf())
5861

@@ -79,6 +82,10 @@ class SpacePresenter(
7982
}
8083
}.collectAsState()
8184

85+
val isSpaceSettingsEnabled by remember {
86+
featureFlagService.isFeatureEnabledFlow(FeatureFlags.SpaceSettings)
87+
}.collectAsState(false)
88+
8289
val currentSpace by spaceRoomList.currentSpaceFlow.collectAsState()
8390
val (joinActions, setJoinActions) = remember { mutableStateOf(emptyMap<RoomId, AsyncAction<Unit>>()) }
8491

@@ -129,6 +136,7 @@ class SpacePresenter(
129136
joinActions = joinActions.toImmutableMap(),
130137
acceptDeclineInviteState = acceptDeclineInviteState,
131138
topicViewerState = topicViewerState,
139+
canAccessSpaceSettings = isSpaceSettingsEnabled,
132140
eventSink = ::handleEvent,
133141
)
134142
}

features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ data class SpaceState(
2626
val joinActions: ImmutableMap<RoomId, AsyncAction<Unit>>,
2727
val acceptDeclineInviteState: AcceptDeclineInviteState,
2828
val topicViewerState: TopicViewerState,
29+
val canAccessSpaceSettings: Boolean,
2930
val eventSink: (SpaceEvents) -> Unit
3031
) {
3132
fun isJoining(spaceId: RoomId): Boolean = joinActions[spaceId] == AsyncAction.Loading

features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ fun aSpaceState(
5353
hasMoreToLoad: Boolean = true,
5454
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
5555
topicViewerState: TopicViewerState = TopicViewerState.Hidden,
56+
canAccessSpaceSettings: Boolean = true,
5657
eventSink: (SpaceEvents) -> Unit = { },
5758
) = SpaceState(
5859
currentSpace = parentSpace,
@@ -63,6 +64,7 @@ fun aSpaceState(
6364
joinActions = joinActions.toImmutableMap(),
6465
acceptDeclineInviteState = acceptDeclineInviteState,
6566
topicViewerState = topicViewerState,
67+
canAccessSpaceSettings = canAccessSpaceSettings,
6668
eventSink = eventSink,
6769
)
6870

features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ fun SpaceView(
8888
topBar = {
8989
SpaceViewTopBar(
9090
currentSpace = state.currentSpace,
91+
canAccessSpaceSettings = state.canAccessSpaceSettings,
9192
onBackClick = onBackClick,
9293
onLeaveSpaceClick = onLeaveSpaceClick,
9394
onShareSpace = onShareSpace,
@@ -255,6 +256,7 @@ private fun LoadingMoreIndicator(
255256
@Composable
256257
private fun SpaceViewTopBar(
257258
currentSpace: SpaceRoom?,
259+
canAccessSpaceSettings: Boolean,
258260
onBackClick: () -> Unit,
259261
onLeaveSpaceClick: () -> Unit,
260262
onDetailsClick: () -> Unit,
@@ -275,8 +277,7 @@ private fun SpaceViewTopBar(
275277
avatarData = currentSpace.getAvatarData(AvatarSize.TimelineRoom),
276278
modifier = Modifier
277279
.clip(roundedCornerShape)
278-
// TODO enable when screen ready for space
279-
.clickable(enabled = false, onClick = onDetailsClick)
280+
.clickable(enabled = canAccessSpaceSettings, onClick = onDetailsClick)
280281
)
281282
}
282283
},
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Copyright (c) 2025 Element Creations Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
package io.element.android.features.space.impl.settings
9+
10+
import android.os.Parcelable
11+
import androidx.compose.runtime.Composable
12+
import androidx.compose.ui.Modifier
13+
import com.bumble.appyx.core.modality.BuildContext
14+
import com.bumble.appyx.core.node.Node
15+
import com.bumble.appyx.core.plugin.Plugin
16+
import com.bumble.appyx.navmodel.backstack.BackStack
17+
import com.bumble.appyx.navmodel.backstack.operation.push
18+
import dev.zacsweers.metro.Assisted
19+
import dev.zacsweers.metro.AssistedInject
20+
import io.element.android.annotations.ContributesNode
21+
import io.element.android.features.rolesandpermissions.api.RolesAndPermissionsEntryPoint
22+
import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyEntryPoint
23+
import io.element.android.features.space.impl.di.SpaceFlowScope
24+
import io.element.android.libraries.architecture.BackstackView
25+
import io.element.android.libraries.architecture.BaseFlowNode
26+
import io.element.android.libraries.architecture.callback
27+
import io.element.android.libraries.architecture.createNode
28+
import kotlinx.parcelize.Parcelize
29+
30+
@ContributesNode(SpaceFlowScope::class)
31+
@AssistedInject
32+
class SpaceSettingsFlowNode(
33+
@Assisted buildContext: BuildContext,
34+
@Assisted plugins: List<Plugin>,
35+
private val securityAndPrivacyEntryPoint: SecurityAndPrivacyEntryPoint,
36+
private val rolesAndPermissionsEntryPoint: RolesAndPermissionsEntryPoint,
37+
) : BaseFlowNode<SpaceSettingsFlowNode.NavTarget>(
38+
backstack = BackStack(
39+
initialElement = NavTarget.Root,
40+
savedStateMap = buildContext.savedStateMap,
41+
),
42+
buildContext = buildContext,
43+
plugins = plugins,
44+
) {
45+
interface Callback : Plugin {
46+
fun navigateToSpaceMembers()
47+
fun startLeaveSpaceFlow()
48+
fun closeSettings()
49+
}
50+
51+
sealed interface NavTarget : Parcelable {
52+
@Parcelize
53+
data object Root : NavTarget
54+
55+
@Parcelize
56+
data object SecurityAndPrivacy : NavTarget
57+
58+
@Parcelize
59+
data object RolesAndPermissions : NavTarget
60+
}
61+
62+
private val callback: Callback = callback()
63+
64+
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
65+
return when (navTarget) {
66+
is NavTarget.Root -> {
67+
val callback = object : SpaceSettingsNode.Callback {
68+
override fun closeSettings() {
69+
callback.closeSettings()
70+
}
71+
72+
override fun navigateToEditDetails() {
73+
// TODO
74+
}
75+
76+
override fun navigateToSpaceMembers() {
77+
callback.navigateToSpaceMembers()
78+
}
79+
80+
override fun navigateToRolesAndPermissions() {
81+
backstack.push(NavTarget.RolesAndPermissions)
82+
}
83+
84+
override fun navigateToSecurityAndPrivacy() {
85+
backstack.push(NavTarget.SecurityAndPrivacy)
86+
}
87+
88+
override fun startLeaveSpaceFlow() {
89+
callback.startLeaveSpaceFlow()
90+
}
91+
}
92+
createNode<SpaceSettingsNode>(
93+
buildContext = buildContext,
94+
plugins = listOf(callback),
95+
)
96+
}
97+
is NavTarget.SecurityAndPrivacy -> {
98+
securityAndPrivacyEntryPoint.createNode(
99+
parentNode = this,
100+
buildContext = buildContext,
101+
)
102+
}
103+
is NavTarget.RolesAndPermissions -> {
104+
rolesAndPermissionsEntryPoint.createNode(
105+
parentNode = this,
106+
buildContext = buildContext,
107+
)
108+
}
109+
}
110+
}
111+
112+
@Composable
113+
override fun View(modifier: Modifier) {
114+
BackstackView(modifier)
115+
}
116+
}

features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsNode.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class SpaceSettingsNode(
3232
interface Callback : Plugin {
3333
fun closeSettings()
3434

35-
fun navigateToSpaceInfo()
35+
fun navigateToEditDetails()
3636
fun navigateToSpaceMembers()
3737
fun navigateToRolesAndPermissions()
3838
fun navigateToSecurityAndPrivacy()
@@ -48,7 +48,7 @@ class SpaceSettingsNode(
4848
SpaceSettingsView(
4949
state = state,
5050
modifier = modifier,
51-
onSpaceInfoClick = callback::navigateToSpaceInfo,
51+
onSpaceInfoClick = callback::navigateToEditDetails,
5252
onBackClick = callback::closeSettings,
5353
onMembersClick = callback::navigateToSpaceMembers,
5454
onRolesAndPermissionsClick = callback::navigateToRolesAndPermissions,

features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import io.element.android.features.invite.api.toInviteData
1919
import io.element.android.features.invite.test.InMemorySeenInvitesStore
2020
import io.element.android.libraries.architecture.AsyncAction
2121
import io.element.android.libraries.architecture.Presenter
22+
import io.element.android.libraries.featureflag.api.FeatureFlags
23+
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
2224
import io.element.android.libraries.matrix.api.MatrixClient
2325
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
2426
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
@@ -62,11 +64,22 @@ class SpacePresenterTest {
6264
assertThat(state.joinActions).isEmpty()
6365
assertThat(state.acceptDeclineInviteState).isEqualTo(anAcceptDeclineInviteState())
6466
assertThat(state.topicViewerState).isEqualTo(TopicViewerState.Hidden)
67+
assertThat(state.canAccessSpaceSettings).isFalse()
6568
advanceUntilIdle()
6669
paginateResult.assertions().isCalledOnce()
6770
}
6871
}
6972

73+
@Test
74+
fun `present - canAccessSpaceSettings when space settings ff is enabled`() = runTest {
75+
val presenter = createSpacePresenter(spaceSettingsEnabled = true)
76+
presenter.test {
77+
skipItems(1)
78+
val state = awaitItem()
79+
assertThat(state.canAccessSpaceSettings).isTrue()
80+
}
81+
}
82+
7083
@Test
7184
fun `present - load more`() = runTest {
7285
val paginateResult = lambdaRecorder<Result<Unit>> {
@@ -328,6 +341,7 @@ class SpacePresenterTest {
328341
lambda = { _, _, _ -> Result.success(Unit) },
329342
),
330343
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
344+
spaceSettingsEnabled: Boolean = false,
331345
): SpacePresenter {
332346
return SpacePresenter(
333347
client = client,
@@ -336,6 +350,11 @@ class SpacePresenterTest {
336350
joinRoom = joinRoom,
337351
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
338352
sessionCoroutineScope = backgroundScope,
353+
featureFlagService = FakeFeatureFlagService(
354+
initialState = mapOf(
355+
FeatureFlags.SpaceSettings.key to spaceSettingsEnabled,
356+
)
357+
),
339358
)
340359
}
341360
}

libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@ enum class FeatureFlags(
7474
key = "feature.space",
7575
title = "Spaces",
7676
defaultValue = { true },
77+
isFinished = true,
78+
),
79+
SpaceSettings(
80+
key = "feature.spaceSettings",
81+
title = "Space settings",
82+
description = "Allow managing space settings such as details, permissions and privacy.",
83+
defaultValue = { false },
7784
isFinished = false,
7885
),
7986
PrintLogsToLogcat(

0 commit comments

Comments
 (0)