Skip to content

Commit 7406259

Browse files
committed
Leave space - Add screen to leave a space.
1 parent 68fa054 commit 7406259

File tree

28 files changed

+1153
-50
lines changed

28 files changed

+1153
-50
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
@Inject
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: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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.previewutils.room.aSpaceRoom
29+
import kotlinx.collections.immutable.ImmutableList
30+
import kotlinx.collections.immutable.toPersistentList
31+
import kotlinx.coroutines.CoroutineScope
32+
import kotlinx.coroutines.delay
33+
import kotlinx.coroutines.launch
34+
import kotlin.jvm.optionals.getOrNull
35+
36+
@Inject
37+
class LeaveSpacePresenter(
38+
@Assisted private val inputs: SpaceEntryPoint.Inputs,
39+
private val matrixClient: MatrixClient,
40+
) : Presenter<LeaveSpaceState> {
41+
@AssistedFactory
42+
fun interface Factory {
43+
fun create(inputs: SpaceEntryPoint.Inputs): LeaveSpacePresenter
44+
}
45+
46+
private val spaceRoomList = matrixClient.spaceService.spaceRoomList(inputs.roomId)
47+
48+
@Composable
49+
override fun present(): LeaveSpaceState {
50+
val coroutineScope = rememberCoroutineScope()
51+
val currentSpace by spaceRoomList.currentSpaceFlow.collectAsState()
52+
val leaveSpaceAction = remember {
53+
mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
54+
}
55+
val selectedRoomIds = remember {
56+
mutableStateOf<Set<RoomId>>(emptySet())
57+
}
58+
val joinedSpaceRooms by produceState(emptyList()) {
59+
// TODO Get the joined room from the SDK, should also have the
60+
val rooms = listOf(
61+
aSpaceRoom(
62+
roomId = RoomId("!roomId1:example.com"),
63+
),
64+
aSpaceRoom(
65+
roomId = RoomId("!roomId2:example.com"),
66+
),
67+
)
68+
value = rooms
69+
}
70+
val selectableSpaceRooms by produceState<AsyncData<ImmutableList<SelectableSpaceRoom>>>(
71+
initialValue = AsyncData.Uninitialized,
72+
key1 = joinedSpaceRooms,
73+
key2 = selectedRoomIds.value,
74+
) {
75+
value = AsyncData.Success(
76+
joinedSpaceRooms.map {
77+
SelectableSpaceRoom(
78+
it,
79+
// TODO Get this value from the SDK
80+
isLastAdmin = false,
81+
selectedRoomIds.value.contains(it.roomId),
82+
)
83+
}.toPersistentList()
84+
)
85+
}
86+
87+
fun handleEvents(event: LeaveSpaceEvents) {
88+
when (event) {
89+
LeaveSpaceEvents.DeselectAllRooms -> selectedRoomIds.value = emptySet()
90+
LeaveSpaceEvents.SelectAllRooms -> {
91+
selectedRoomIds.value = selectableSpaceRooms.dataOrNull()
92+
.orEmpty()
93+
.filter { it.isLastAdmin.not() }
94+
.map { it.spaceRoom.roomId }
95+
.toSet()
96+
}
97+
is LeaveSpaceEvents.ToggleRoomSelection -> {
98+
val currentSet = selectedRoomIds.value
99+
selectedRoomIds.value = if (currentSet.contains(event.roomId)) {
100+
currentSet - event.roomId
101+
} else {
102+
currentSet + event.roomId
103+
}
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+
delay(1000)
130+
val room = matrixClient.getRoom(inputs.roomId)
131+
?: return@runUpdatingState Result.failure(Exception("Room not found"))
132+
room.leave()
133+
}
134+
}
135+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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 io.element.android.libraries.core.bool.orFalse
13+
import kotlinx.collections.immutable.ImmutableList
14+
15+
data class LeaveSpaceState(
16+
val spaceName: String?,
17+
val selectableSpaceRooms: AsyncData<ImmutableList<SelectableSpaceRoom>>,
18+
val leaveSpaceAction: AsyncAction<Unit>,
19+
val eventSink: (LeaveSpaceEvents) -> Unit,
20+
) {
21+
val showQuickAction = selectableSpaceRooms.dataOrNull().orEmpty().any { !it.isLastAdmin }
22+
val hasOnlyLastAdminRoom = selectableSpaceRooms.dataOrNull()
23+
?.let { rooms ->
24+
rooms.isNotEmpty() && rooms.all { it.isLastAdmin }
25+
}
26+
.orFalse()
27+
val numberOfSelectRooms = selectableSpaceRooms.dataOrNull().orEmpty().count { it.isSelected }
28+
29+
val areAllSelected = selectableSpaceRooms.dataOrNull()
30+
?.filter { !it.isLastAdmin }
31+
?.let { rooms ->
32+
rooms.isNotEmpty() && rooms.all { it.isSelected }
33+
}
34+
.orFalse()
35+
}

0 commit comments

Comments
 (0)