Skip to content

Commit 59df60a

Browse files
authored
Merge pull request #5354 from element-hq/feature/bma/leaveSpace
Leave space - UI
2 parents 2dd2a52 + ef36922 commit 59df60a

File tree

75 files changed

+1281
-98
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

75 files changed

+1281
-98
lines changed

appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,17 @@ class LoggedInEventProcessor(
3131
observingJob = roomMembershipObserver.updates
3232
.filter { !it.isUserInRoom }
3333
.distinctUntilChanged()
34-
.onEach {
35-
when (it.change) {
36-
MembershipChange.LEFT -> displayMessage(CommonStrings.common_current_user_left_room)
34+
.onEach { roomMemberShipUpdate ->
35+
when (roomMemberShipUpdate.change) {
36+
MembershipChange.LEFT -> {
37+
displayMessage(
38+
if (roomMemberShipUpdate.isSpace) {
39+
CommonStrings.common_current_user_left_space
40+
} else {
41+
CommonStrings.common_current_user_left_room
42+
}
43+
)
44+
}
3745
MembershipChange.INVITATION_REJECTED -> displayMessage(CommonStrings.common_current_user_rejected_invite)
3846
MembershipChange.KNOCK_RETRACTED -> displayMessage(CommonStrings.common_current_user_canceled_knock)
3947
else -> Unit

features/rageshake/impl/src/main/res/values/localazy.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,7 @@
1414
<string name="screen_bug_report_include_screenshot">"Send screenshot"</string>
1515
<string name="screen_bug_report_logs_description">"Logs will be included with your message to make sure that everything is working properly. To send your message without logs, turn off this setting."</string>
1616
<string name="screen_bug_report_rash_logs_alert_title">"%1$s crashed the last time it was used. Would you like to share a crash report with us?"</string>
17+
<string name="screen_bug_report_send_notification_settings_description">"If you are having issues with notifications, uploading the notification settings can help us pinpoint the root cause."</string>
18+
<string name="screen_bug_report_send_notification_settings_title">"Send notification settings"</string>
1719
<string name="screen_bug_report_view_logs">"View logs"</string>
1820
</resources>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class DefaultSpaceEntryPoint : SpaceEntryPoint {
3333
}
3434

3535
override fun build(): Node {
36-
return parentNode.createNode<SpaceNode>(buildContext, plugins = plugins.toList())
36+
return parentNode.createNode<SpaceFlowNode>(buildContext, plugins = plugins.toList())
3737
}
3838
}
3939
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright 2025 New Vector 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+
@file:OptIn(ExperimentalMaterial3Api::class)
9+
10+
package io.element.android.features.space.impl
11+
12+
import android.os.Parcelable
13+
import androidx.compose.material3.ExperimentalMaterial3Api
14+
import androidx.compose.runtime.Composable
15+
import androidx.compose.ui.Modifier
16+
import com.bumble.appyx.core.modality.BuildContext
17+
import com.bumble.appyx.core.node.Node
18+
import com.bumble.appyx.core.plugin.Plugin
19+
import com.bumble.appyx.navmodel.backstack.BackStack
20+
import com.bumble.appyx.navmodel.backstack.operation.push
21+
import dev.zacsweers.metro.Assisted
22+
import dev.zacsweers.metro.Inject
23+
import io.element.android.annotations.ContributesNode
24+
import io.element.android.features.space.api.SpaceEntryPoint
25+
import io.element.android.features.space.impl.leave.LeaveSpaceNode
26+
import io.element.android.features.space.impl.root.SpaceNode
27+
import io.element.android.libraries.architecture.BackstackView
28+
import io.element.android.libraries.architecture.BaseFlowNode
29+
import io.element.android.libraries.architecture.createNode
30+
import io.element.android.libraries.architecture.inputs
31+
import io.element.android.libraries.di.SessionScope
32+
import io.element.android.libraries.matrix.api.core.RoomId
33+
import kotlinx.parcelize.Parcelize
34+
35+
@ContributesNode(SessionScope::class)
36+
@Inject
37+
class SpaceFlowNode(
38+
@Assisted val buildContext: BuildContext,
39+
@Assisted plugins: List<Plugin>,
40+
) : BaseFlowNode<SpaceFlowNode.NavTarget>(
41+
backstack = BackStack(
42+
initialElement = NavTarget.Root,
43+
savedStateMap = buildContext.savedStateMap,
44+
),
45+
buildContext = buildContext,
46+
plugins = plugins,
47+
) {
48+
private val inputs: SpaceEntryPoint.Inputs = inputs()
49+
private val callback = plugins.filterIsInstance<SpaceEntryPoint.Callback>().single()
50+
51+
sealed interface NavTarget : Parcelable {
52+
@Parcelize
53+
data object Root : NavTarget
54+
55+
@Parcelize
56+
data object Leave : NavTarget
57+
}
58+
59+
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
60+
return when (navTarget) {
61+
NavTarget.Leave -> {
62+
createNode<LeaveSpaceNode>(buildContext, listOf(inputs))
63+
}
64+
NavTarget.Root -> {
65+
val callback = object : SpaceNode.Callback {
66+
override fun onOpenRoom(roomId: RoomId, viaParameters: List<String>) {
67+
callback.onOpenRoom(roomId, viaParameters)
68+
}
69+
70+
override fun onLeaveSpace() {
71+
backstack.push(NavTarget.Leave)
72+
}
73+
}
74+
createNode<SpaceNode>(buildContext, listOf(inputs, callback))
75+
}
76+
}
77+
}
78+
79+
@Composable
80+
override fun View(modifier: Modifier) = BackstackView()
81+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Copyright 2025 New Vector 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.leave
9+
10+
import io.element.android.libraries.matrix.api.core.RoomId
11+
12+
sealed interface LeaveSpaceEvents {
13+
data object SelectAllRooms : LeaveSpaceEvents
14+
data object DeselectAllRooms : LeaveSpaceEvents
15+
data class ToggleRoomSelection(val roomId: RoomId) : LeaveSpaceEvents
16+
data object LeaveSpace : LeaveSpaceEvents
17+
data object CloseError : LeaveSpaceEvents
18+
}

features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceNode.kt renamed to features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceNode.kt

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* Please see LICENSE files in the repository root for full details.
66
*/
77

8-
package io.element.android.features.space.impl
8+
package io.element.android.features.space.impl.leave
99

1010
import androidx.compose.runtime.Composable
1111
import androidx.compose.ui.Modifier
@@ -21,24 +21,20 @@ import io.element.android.libraries.di.SessionScope
2121

2222
@ContributesNode(SessionScope::class)
2323
@AssistedInject
24-
class SpaceNode(
24+
class LeaveSpaceNode(
2525
@Assisted buildContext: BuildContext,
2626
@Assisted plugins: List<Plugin>,
27-
presenterFactory: SpacePresenter.Factory,
27+
presenterFactory: LeaveSpacePresenter.Factory,
2828
) : Node(buildContext, plugins = plugins) {
2929
private val inputs: SpaceEntryPoint.Inputs = inputs()
30-
private val callback = plugins.filterIsInstance<SpaceEntryPoint.Callback>().single()
3130
private val presenter = presenterFactory.create(inputs)
3231

3332
@Composable
3433
override fun View(modifier: Modifier) {
3534
val state = presenter.present()
36-
SpaceView(
35+
LeaveSpaceView(
3736
state = state,
38-
onBackClick = ::navigateUp,
39-
onRoomClick = { spaceRoom ->
40-
callback.onOpenRoom(spaceRoom.roomId, spaceRoom.via)
41-
},
37+
onCancel = ::navigateUp,
4238
modifier = modifier
4339
)
4440
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
* Copyright 2025 New Vector 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.leave
9+
10+
import androidx.compose.runtime.Composable
11+
import androidx.compose.runtime.MutableState
12+
import androidx.compose.runtime.collectAsState
13+
import androidx.compose.runtime.getValue
14+
import androidx.compose.runtime.mutableStateOf
15+
import androidx.compose.runtime.produceState
16+
import androidx.compose.runtime.remember
17+
import androidx.compose.runtime.rememberCoroutineScope
18+
import dev.zacsweers.metro.Assisted
19+
import dev.zacsweers.metro.AssistedFactory
20+
import dev.zacsweers.metro.Inject
21+
import io.element.android.features.space.api.SpaceEntryPoint
22+
import io.element.android.libraries.architecture.AsyncAction
23+
import io.element.android.libraries.architecture.AsyncData
24+
import io.element.android.libraries.architecture.Presenter
25+
import io.element.android.libraries.architecture.runUpdatingState
26+
import io.element.android.libraries.matrix.api.MatrixClient
27+
import io.element.android.libraries.matrix.api.core.RoomId
28+
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
29+
import kotlinx.collections.immutable.ImmutableList
30+
import kotlinx.collections.immutable.ImmutableSet
31+
import kotlinx.collections.immutable.persistentSetOf
32+
import kotlinx.collections.immutable.toPersistentList
33+
import kotlinx.collections.immutable.toPersistentSet
34+
import kotlinx.coroutines.CoroutineScope
35+
import kotlinx.coroutines.launch
36+
import kotlin.jvm.optionals.getOrNull
37+
38+
@Inject
39+
class LeaveSpacePresenter(
40+
@Assisted private val inputs: SpaceEntryPoint.Inputs,
41+
matrixClient: MatrixClient,
42+
) : Presenter<LeaveSpaceState> {
43+
@AssistedFactory
44+
fun interface Factory {
45+
fun create(inputs: SpaceEntryPoint.Inputs): LeaveSpacePresenter
46+
}
47+
48+
private val spaceRoomList = matrixClient.spaceService.spaceRoomList(inputs.roomId)
49+
50+
@Composable
51+
override fun present(): LeaveSpaceState {
52+
val coroutineScope = rememberCoroutineScope()
53+
val currentSpace by spaceRoomList.currentSpaceFlow.collectAsState()
54+
val leaveSpaceAction = remember {
55+
mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
56+
}
57+
val selectedRoomIds = remember {
58+
mutableStateOf<ImmutableSet<RoomId>>(persistentSetOf())
59+
}
60+
val joinedSpaceRooms by produceState(emptyList()) {
61+
// TODO Get the joined room from the SDK, should also have the isLastAdmin boolean
62+
val rooms = emptyList<SpaceRoom>()
63+
// By default select all rooms
64+
selectedRoomIds.value = rooms.map { it.roomId }.toPersistentSet()
65+
value = rooms
66+
}
67+
val selectableSpaceRooms by produceState<AsyncData<ImmutableList<SelectableSpaceRoom>>>(
68+
initialValue = AsyncData.Uninitialized,
69+
key1 = joinedSpaceRooms,
70+
key2 = selectedRoomIds.value,
71+
) {
72+
value = AsyncData.Success(
73+
joinedSpaceRooms.map {
74+
SelectableSpaceRoom(
75+
spaceRoom = it,
76+
// TODO Get this value from the SDK
77+
isLastAdmin = false,
78+
isSelected = selectedRoomIds.value.contains(it.roomId),
79+
)
80+
}.toPersistentList()
81+
)
82+
}
83+
84+
fun handleEvents(event: LeaveSpaceEvents) {
85+
when (event) {
86+
LeaveSpaceEvents.DeselectAllRooms -> {
87+
selectedRoomIds.value = persistentSetOf()
88+
}
89+
LeaveSpaceEvents.SelectAllRooms -> {
90+
selectedRoomIds.value = selectableSpaceRooms.dataOrNull()
91+
.orEmpty()
92+
.filter { it.isLastAdmin.not() }
93+
.map { it.spaceRoom.roomId }
94+
.toPersistentSet()
95+
}
96+
is LeaveSpaceEvents.ToggleRoomSelection -> {
97+
val currentSet = selectedRoomIds.value
98+
selectedRoomIds.value = if (currentSet.contains(event.roomId)) {
99+
currentSet - event.roomId
100+
} else {
101+
currentSet + event.roomId
102+
}
103+
.toPersistentSet()
104+
}
105+
LeaveSpaceEvents.LeaveSpace -> coroutineScope.leaveSpace(
106+
leaveSpaceAction = leaveSpaceAction,
107+
selectedRoomIds = selectedRoomIds.value,
108+
)
109+
LeaveSpaceEvents.CloseError -> {
110+
leaveSpaceAction.value = AsyncAction.Uninitialized
111+
}
112+
}
113+
}
114+
115+
return LeaveSpaceState(
116+
spaceName = currentSpace.getOrNull()?.name,
117+
selectableSpaceRooms = selectableSpaceRooms,
118+
leaveSpaceAction = leaveSpaceAction.value,
119+
eventSink = ::handleEvents,
120+
)
121+
}
122+
123+
private fun CoroutineScope.leaveSpace(
124+
leaveSpaceAction: MutableState<AsyncAction<Unit>>,
125+
@Suppress("unused") selectedRoomIds: Set<RoomId>,
126+
) = launch {
127+
runUpdatingState(leaveSpaceAction) {
128+
// TODO SDK API call to leave all the rooms and space
129+
Result.failure(Exception("Not implemented"))
130+
}
131+
}
132+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2025 New Vector 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.leave
9+
10+
import io.element.android.libraries.architecture.AsyncAction
11+
import io.element.android.libraries.architecture.AsyncData
12+
import kotlinx.collections.immutable.ImmutableList
13+
14+
data class LeaveSpaceState(
15+
val spaceName: String?,
16+
val selectableSpaceRooms: AsyncData<ImmutableList<SelectableSpaceRoom>>,
17+
val leaveSpaceAction: AsyncAction<Unit>,
18+
val eventSink: (LeaveSpaceEvents) -> Unit,
19+
) {
20+
private val rooms = selectableSpaceRooms.dataOrNull().orEmpty()
21+
private val partition = rooms.partition { it.isLastAdmin }
22+
private val lastAdminRooms = partition.first
23+
private val selectableRooms = partition.second
24+
25+
/**
26+
* True if we should show the quick action to select/deselect all rooms.
27+
*/
28+
val showQuickAction = selectableRooms.isNotEmpty()
29+
30+
/**
31+
* True if there all the selectable rooms are selected.
32+
*/
33+
val areAllSelected = selectableRooms.all { it.isSelected }
34+
35+
/**
36+
* True if there are rooms but the user is the last admin in all of them.
37+
*/
38+
val hasOnlyLastAdminRoom = lastAdminRooms.isNotEmpty() && selectableRooms.isEmpty()
39+
40+
/**
41+
* Number of selected rooms.
42+
*/
43+
val selectedRoomsCount = selectableRooms.count { it.isSelected }
44+
}

0 commit comments

Comments
 (0)