Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,17 @@ class LoggedInEventProcessor(
observingJob = roomMembershipObserver.updates
.filter { !it.isUserInRoom }
.distinctUntilChanged()
.onEach {
when (it.change) {
MembershipChange.LEFT -> displayMessage(CommonStrings.common_current_user_left_room)
.onEach { roomMemberShipUpdate ->
when (roomMemberShipUpdate.change) {
MembershipChange.LEFT -> {
displayMessage(
if (roomMemberShipUpdate.isSpace) {
CommonStrings.common_current_user_left_space
} else {
CommonStrings.common_current_user_left_room
}
)
}
MembershipChange.INVITATION_REJECTED -> displayMessage(CommonStrings.common_current_user_rejected_invite)
MembershipChange.KNOCK_RETRACTED -> displayMessage(CommonStrings.common_current_user_canceled_knock)
else -> Unit
Expand Down
2 changes: 2 additions & 0 deletions features/rageshake/impl/src/main/res/values/localazy.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,7 @@
<string name="screen_bug_report_include_screenshot">"Send screenshot"</string>
<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>
<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>
<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>
<string name="screen_bug_report_send_notification_settings_title">"Send notification settings"</string>
<string name="screen_bug_report_view_logs">"View logs"</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class DefaultSpaceEntryPoint : SpaceEntryPoint {
}

override fun build(): Node {
return parentNode.createNode<SpaceNode>(buildContext, plugins = plugins.toList())
return parentNode.createNode<SpaceFlowNode>(buildContext, plugins = plugins.toList())
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/

@file:OptIn(ExperimentalMaterial3Api::class)

package io.element.android.features.space.impl

import android.os.Parcelable
import androidx.compose.material3.ExperimentalMaterial3Api
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.Inject
import io.element.android.annotations.ContributesNode
import io.element.android.features.space.api.SpaceEntryPoint
import io.element.android.features.space.impl.leave.LeaveSpaceNode
import io.element.android.features.space.impl.root.SpaceNode
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.parcelize.Parcelize

@ContributesNode(SessionScope::class)
@Inject
class SpaceFlowNode(
@Assisted val buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
) : BaseFlowNode<SpaceFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.Root,
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext,
plugins = plugins,
) {
private val inputs: SpaceEntryPoint.Inputs = inputs()
private val callback = plugins.filterIsInstance<SpaceEntryPoint.Callback>().single()

sealed interface NavTarget : Parcelable {
@Parcelize
data object Root : NavTarget

@Parcelize
data object Leave : NavTarget
}

override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
NavTarget.Leave -> {
createNode<LeaveSpaceNode>(buildContext, listOf(inputs))
}
NavTarget.Root -> {
val callback = object : SpaceNode.Callback {
override fun onOpenRoom(roomId: RoomId, viaParameters: List<String>) {
callback.onOpenRoom(roomId, viaParameters)
}

override fun onLeaveSpace() {
backstack.push(NavTarget.Leave)
}
}
createNode<SpaceNode>(buildContext, listOf(inputs, callback))
}
}
}

@Composable
override fun View(modifier: Modifier) = BackstackView()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright 2025 New Vector 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.leave

import io.element.android.libraries.matrix.api.core.RoomId

sealed interface LeaveSpaceEvents {
data object SelectAllRooms : LeaveSpaceEvents
data object DeselectAllRooms : LeaveSpaceEvents
data class ToggleRoomSelection(val roomId: RoomId) : LeaveSpaceEvents
data object LeaveSpace : LeaveSpaceEvents
data object CloseError : LeaveSpaceEvents
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/

package io.element.android.features.space.impl
package io.element.android.features.space.impl.leave

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

@ContributesNode(SessionScope::class)
@AssistedInject
class SpaceNode(
class LeaveSpaceNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
presenterFactory: SpacePresenter.Factory,
presenterFactory: LeaveSpacePresenter.Factory,
) : Node(buildContext, plugins = plugins) {
private val inputs: SpaceEntryPoint.Inputs = inputs()
private val callback = plugins.filterIsInstance<SpaceEntryPoint.Callback>().single()
private val presenter = presenterFactory.create(inputs)

@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
SpaceView(
LeaveSpaceView(
state = state,
onBackClick = ::navigateUp,
onRoomClick = { spaceRoom ->
callback.onOpenRoom(spaceRoom.roomId, spaceRoom.via)
},
onCancel = ::navigateUp,
modifier = modifier
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* Copyright 2025 New Vector 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.leave

import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.Inject
import io.element.android.features.space.api.SpaceEntryPoint
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runUpdatingState
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.previewutils.room.aSpaceRoom
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.jvm.optionals.getOrNull

@Inject
class LeaveSpacePresenter(
@Assisted private val inputs: SpaceEntryPoint.Inputs,
private val matrixClient: MatrixClient,
) : Presenter<LeaveSpaceState> {
@AssistedFactory
fun interface Factory {
fun create(inputs: SpaceEntryPoint.Inputs): LeaveSpacePresenter
}

private val spaceRoomList = matrixClient.spaceService.spaceRoomList(inputs.roomId)

@Composable
override fun present(): LeaveSpaceState {
val coroutineScope = rememberCoroutineScope()
val currentSpace by spaceRoomList.currentSpaceFlow.collectAsState()
val leaveSpaceAction = remember {
mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
}
val selectedRoomIds = remember {
mutableStateOf<Set<RoomId>>(emptySet())
}
val joinedSpaceRooms by produceState(emptyList()) {
// TODO Get the joined room from the SDK, should also have the
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment seems incomplete, so much suspense 🕵️ .

val rooms = listOf(
aSpaceRoom(
roomId = RoomId("!roomId1:example.com"),
),
aSpaceRoom(
roomId = RoomId("!roomId2:example.com"),
),
)
value = rooms
}
val selectableSpaceRooms by produceState<AsyncData<ImmutableList<SelectableSpaceRoom>>>(
initialValue = AsyncData.Uninitialized,
key1 = joinedSpaceRooms,
key2 = selectedRoomIds.value,
) {
value = AsyncData.Success(
joinedSpaceRooms.map {
SelectableSpaceRoom(
it,
// TODO Get this value from the SDK
isLastAdmin = false,
selectedRoomIds.value.contains(it.roomId),
)
}.toPersistentList()
)
}

fun handleEvents(event: LeaveSpaceEvents) {
when (event) {
LeaveSpaceEvents.DeselectAllRooms -> selectedRoomIds.value = emptySet()
LeaveSpaceEvents.SelectAllRooms -> {
selectedRoomIds.value = selectableSpaceRooms.dataOrNull()
.orEmpty()
.filter { it.isLastAdmin.not() }
.map { it.spaceRoom.roomId }
.toSet()
}
is LeaveSpaceEvents.ToggleRoomSelection -> {
val currentSet = selectedRoomIds.value
selectedRoomIds.value = if (currentSet.contains(event.roomId)) {
currentSet - event.roomId
} else {
currentSet + event.roomId
}
}
LeaveSpaceEvents.LeaveSpace -> coroutineScope.leaveSpace(
leaveSpaceAction = leaveSpaceAction,
selectedRoomIds = selectedRoomIds.value,
)
LeaveSpaceEvents.CloseError -> {
leaveSpaceAction.value = AsyncAction.Uninitialized
}
}
}

return LeaveSpaceState(
spaceName = currentSpace.getOrNull()?.name,
selectableSpaceRooms = selectableSpaceRooms,
leaveSpaceAction = leaveSpaceAction.value,
eventSink = ::handleEvents,
)
}

private fun CoroutineScope.leaveSpace(
leaveSpaceAction: MutableState<AsyncAction<Unit>>,
@Suppress("unused") selectedRoomIds: Set<RoomId>,
) = launch {
runUpdatingState(leaveSpaceAction) {
// TODO SDK API call to leave all the rooms and space
delay(1000)
val room = matrixClient.getRoom(inputs.roomId)
?: return@runUpdatingState Result.failure(Exception("Room not found"))
room.leave()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2025 New Vector 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.leave

import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.bool.orFalse
import kotlinx.collections.immutable.ImmutableList

data class LeaveSpaceState(
val spaceName: String?,
val selectableSpaceRooms: AsyncData<ImmutableList<SelectableSpaceRoom>>,
val leaveSpaceAction: AsyncAction<Unit>,
val eventSink: (LeaveSpaceEvents) -> Unit,
) {
val showQuickAction = selectableSpaceRooms.dataOrNull().orEmpty().any { !it.isLastAdmin }
val hasOnlyLastAdminRoom = selectableSpaceRooms.dataOrNull()
?.let { rooms ->
rooms.isNotEmpty() && rooms.all { it.isLastAdmin }
}
.orFalse()
val numberOfSelectRooms = selectableSpaceRooms.dataOrNull().orEmpty().count { it.isSelected }

val areAllSelected = selectableSpaceRooms.dataOrNull()
?.filter { !it.isLastAdmin }
?.let { rooms ->
rooms.isNotEmpty() && rooms.all { it.isSelected }
}
.orFalse()
}
Loading