Merge pull request #5432 from element-hq/feature/bma/leaveSpace
Leave space: use SDK API.
This commit is contained in:
@@ -10,6 +10,7 @@ package io.element.android.features.space.impl.leave
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
sealed interface LeaveSpaceEvents {
|
||||
data object Retry : LeaveSpaceEvents
|
||||
data object SelectAllRooms : LeaveSpaceEvents
|
||||
data object DeselectAllRooms : LeaveSpaceEvents
|
||||
data class ToggleRoomSelection(val roomId: RoomId) : LeaveSpaceEvents
|
||||
|
||||
@@ -9,21 +9,39 @@ package io.element.android.features.space.impl.leave
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.lifecycle.subscribe
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.space.api.SpaceEntryPoint
|
||||
import io.element.android.features.space.impl.di.SpaceFlowScope
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
|
||||
@ContributesNode(SpaceFlowScope::class)
|
||||
@AssistedInject
|
||||
class LeaveSpaceNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: LeaveSpacePresenter,
|
||||
matrixClient: MatrixClient,
|
||||
presenterFactory: LeaveSpacePresenter.Factory,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
private val inputs: SpaceEntryPoint.Inputs = inputs()
|
||||
private val leaveSpaceHandle = matrixClient.spaceService.getLeaveSpaceHandle(inputs.roomId)
|
||||
private val presenter: LeaveSpacePresenter = presenterFactory.create(leaveSpaceHandle)
|
||||
|
||||
override fun onBuilt() {
|
||||
super.onBuilt()
|
||||
lifecycle.subscribe(
|
||||
onDestroy = {
|
||||
leaveSpaceHandle.close()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
|
||||
@@ -8,92 +8,119 @@
|
||||
package io.element.android.features.space.impl.leave
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import dev.zacsweers.metro.Inject
|
||||
import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
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.map
|
||||
import io.element.android.libraries.architecture.runUpdatingState
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
||||
import io.element.android.libraries.matrix.api.spaces.LeaveSpaceHandle
|
||||
import io.element.android.libraries.matrix.api.spaces.LeaveSpaceRoom
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.collections.immutable.toPersistentSet
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class LeaveSpacePresenter(
|
||||
private val spaceRoomList: SpaceRoomList,
|
||||
@Assisted private val leaveSpaceHandle: LeaveSpaceHandle,
|
||||
) : Presenter<LeaveSpaceState> {
|
||||
@AssistedFactory
|
||||
fun interface Factory {
|
||||
fun create(leaveSpaceHandle: LeaveSpaceHandle): LeaveSpacePresenter
|
||||
}
|
||||
|
||||
data class LeaveSpaceRooms(
|
||||
val current: LeaveSpaceRoom?,
|
||||
val others: List<LeaveSpaceRoom>,
|
||||
)
|
||||
|
||||
@Composable
|
||||
override fun present(): LeaveSpaceState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val currentSpace by spaceRoomList.currentSpaceFlow.collectAsState()
|
||||
var retryCount by remember { mutableIntStateOf(0) }
|
||||
val leaveSpaceAction = remember {
|
||||
mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
|
||||
}
|
||||
val selectedRoomIds = remember {
|
||||
mutableStateOf<ImmutableSet<RoomId>>(persistentSetOf())
|
||||
var selectedRoomIds by remember {
|
||||
mutableStateOf<Collection<RoomId>>(setOf())
|
||||
}
|
||||
val joinedSpaceRooms by produceState(emptyList()) {
|
||||
// TODO Get the joined room from the SDK, should also have the isLastAdmin boolean
|
||||
val rooms = emptyList<SpaceRoom>()
|
||||
// By default select all rooms
|
||||
selectedRoomIds.value = rooms.map { it.roomId }.toPersistentSet()
|
||||
value = rooms
|
||||
var leaveSpaceRooms by remember {
|
||||
mutableStateOf<AsyncData<LeaveSpaceRooms>>(AsyncData.Loading())
|
||||
}
|
||||
val selectableSpaceRooms by produceState<AsyncData<ImmutableList<SelectableSpaceRoom>>>(
|
||||
initialValue = AsyncData.Uninitialized,
|
||||
key1 = joinedSpaceRooms,
|
||||
key2 = selectedRoomIds.value,
|
||||
) {
|
||||
value = AsyncData.Success(
|
||||
joinedSpaceRooms.map {
|
||||
SelectableSpaceRoom(
|
||||
spaceRoom = it,
|
||||
// TODO Get this value from the SDK
|
||||
isLastAdmin = false,
|
||||
isSelected = selectedRoomIds.value.contains(it.roomId),
|
||||
LaunchedEffect(retryCount) {
|
||||
val rooms = leaveSpaceHandle.rooms()
|
||||
val (currentRoom, otherRooms) = rooms.getOrNull()
|
||||
.orEmpty()
|
||||
.partition { it.spaceRoom.roomId == leaveSpaceHandle.id }
|
||||
// By default select all rooms that can be left
|
||||
selectedRoomIds = otherRooms
|
||||
.filter { it.isLastAdmin.not() }
|
||||
.map { it.spaceRoom.roomId }
|
||||
leaveSpaceRooms = rooms.fold(
|
||||
onSuccess = {
|
||||
AsyncData.Success(
|
||||
LeaveSpaceRooms(
|
||||
current = currentRoom.firstOrNull(),
|
||||
others = otherRooms.toImmutableList(),
|
||||
)
|
||||
)
|
||||
}.toPersistentList()
|
||||
},
|
||||
onFailure = { AsyncData.Failure(it) }
|
||||
)
|
||||
}
|
||||
var selectableSpaceRooms by remember {
|
||||
mutableStateOf<AsyncData<ImmutableList<SelectableSpaceRoom>>>(AsyncData.Loading())
|
||||
}
|
||||
LaunchedEffect(selectedRoomIds, leaveSpaceRooms) {
|
||||
selectableSpaceRooms = leaveSpaceRooms.map {
|
||||
it?.others.orEmpty().map { room ->
|
||||
SelectableSpaceRoom(
|
||||
spaceRoom = room.spaceRoom,
|
||||
isLastAdmin = room.isLastAdmin,
|
||||
isSelected = selectedRoomIds.contains(room.spaceRoom.roomId),
|
||||
)
|
||||
}.toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
fun handleEvents(event: LeaveSpaceEvents) {
|
||||
when (event) {
|
||||
LeaveSpaceEvents.Retry -> {
|
||||
leaveSpaceRooms = AsyncData.Loading()
|
||||
retryCount += 1
|
||||
}
|
||||
LeaveSpaceEvents.DeselectAllRooms -> {
|
||||
selectedRoomIds.value = persistentSetOf()
|
||||
selectedRoomIds = persistentSetOf()
|
||||
}
|
||||
LeaveSpaceEvents.SelectAllRooms -> {
|
||||
selectedRoomIds.value = selectableSpaceRooms.dataOrNull()
|
||||
selectedRoomIds = selectableSpaceRooms.dataOrNull()
|
||||
.orEmpty()
|
||||
.filter { it.isLastAdmin.not() }
|
||||
.map { it.spaceRoom.roomId }
|
||||
.toPersistentSet()
|
||||
}
|
||||
is LeaveSpaceEvents.ToggleRoomSelection -> {
|
||||
val currentSet = selectedRoomIds.value
|
||||
selectedRoomIds.value = if (currentSet.contains(event.roomId)) {
|
||||
currentSet - event.roomId
|
||||
selectedRoomIds = if (selectedRoomIds.contains(event.roomId)) {
|
||||
selectedRoomIds - event.roomId
|
||||
} else {
|
||||
currentSet + event.roomId
|
||||
selectedRoomIds + event.roomId
|
||||
}
|
||||
.toPersistentSet()
|
||||
}
|
||||
LeaveSpaceEvents.LeaveSpace -> coroutineScope.leaveSpace(
|
||||
leaveSpaceAction = leaveSpaceAction,
|
||||
selectedRoomIds = selectedRoomIds.value,
|
||||
selectedRoomIds = selectedRoomIds,
|
||||
)
|
||||
LeaveSpaceEvents.CloseError -> {
|
||||
leaveSpaceAction.value = AsyncAction.Uninitialized
|
||||
@@ -102,7 +129,8 @@ class LeaveSpacePresenter(
|
||||
}
|
||||
|
||||
return LeaveSpaceState(
|
||||
spaceName = currentSpace.getOrNull()?.name,
|
||||
spaceName = leaveSpaceRooms.dataOrNull()?.current?.spaceRoom?.name,
|
||||
isLastAdmin = leaveSpaceRooms.dataOrNull()?.current?.isLastAdmin == true,
|
||||
selectableSpaceRooms = selectableSpaceRooms,
|
||||
leaveSpaceAction = leaveSpaceAction.value,
|
||||
eventSink = ::handleEvents,
|
||||
@@ -111,11 +139,10 @@ class LeaveSpacePresenter(
|
||||
|
||||
private fun CoroutineScope.leaveSpace(
|
||||
leaveSpaceAction: MutableState<AsyncAction<Unit>>,
|
||||
@Suppress("unused") selectedRoomIds: Set<RoomId>,
|
||||
selectedRoomIds: Collection<RoomId>,
|
||||
) = launch {
|
||||
runUpdatingState(leaveSpaceAction) {
|
||||
// TODO SDK API call to leave all the rooms and space
|
||||
Result.failure(Exception("Not implemented"))
|
||||
leaveSpaceHandle.leave(selectedRoomIds.toList())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class LeaveSpaceState(
|
||||
val spaceName: String?,
|
||||
val isLastAdmin: Boolean,
|
||||
val selectableSpaceRooms: AsyncData<ImmutableList<SelectableSpaceRoom>>,
|
||||
val leaveSpaceAction: AsyncAction<Unit>,
|
||||
val eventSink: (LeaveSpaceEvents) -> Unit,
|
||||
@@ -25,7 +26,12 @@ data class LeaveSpaceState(
|
||||
/**
|
||||
* True if we should show the quick action to select/deselect all rooms.
|
||||
*/
|
||||
val showQuickAction = selectableRooms.isNotEmpty()
|
||||
val showQuickAction = isLastAdmin.not() && selectableRooms.isNotEmpty()
|
||||
|
||||
/**
|
||||
* True if we should show the leave button.
|
||||
*/
|
||||
val showLeaveButton = isLastAdmin.not() && selectableSpaceRooms is AsyncData.Success
|
||||
|
||||
/**
|
||||
* True if there all the selectable rooms are selected.
|
||||
|
||||
@@ -105,15 +105,20 @@ class LeaveSpaceStateProvider : PreviewParameterProvider<LeaveSpaceState> {
|
||||
aLeaveSpaceState(
|
||||
selectableSpaceRooms = AsyncData.Failure(Exception("An error")),
|
||||
),
|
||||
aLeaveSpaceState(
|
||||
isLastAdmin = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun aLeaveSpaceState(
|
||||
spaceName: String? = "Space name",
|
||||
isLastAdmin: Boolean = false,
|
||||
selectableSpaceRooms: AsyncData<ImmutableList<SelectableSpaceRoom>> = AsyncData.Uninitialized,
|
||||
leaveSpaceAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
) = LeaveSpaceState(
|
||||
spaceName = spaceName,
|
||||
isLastAdmin = isLastAdmin,
|
||||
selectableSpaceRooms = selectableSpaceRooms,
|
||||
leaveSpaceAction = leaveSpaceAction,
|
||||
eventSink = { }
|
||||
|
||||
@@ -9,8 +9,10 @@
|
||||
|
||||
package io.element.android.features.space.impl.leave
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -71,6 +73,12 @@ fun LeaveSpaceView(
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
LeaveSpaceHeader(
|
||||
state = state,
|
||||
onBackClick = onCancel,
|
||||
)
|
||||
},
|
||||
containerColor = ElementTheme.colors.bgCanvasDefault,
|
||||
) { padding ->
|
||||
Column(
|
||||
@@ -79,45 +87,44 @@ fun LeaveSpaceView(
|
||||
.imePadding()
|
||||
.consumeWindowInsets(padding)
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
LeaveSpaceHeader(
|
||||
state = state,
|
||||
onBackClick = onCancel,
|
||||
)
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
) {
|
||||
when (state.selectableSpaceRooms) {
|
||||
is AsyncData.Success -> {
|
||||
// List rooms where the user is the only admin
|
||||
state.selectableSpaceRooms.data.forEach { selectableSpaceRoom ->
|
||||
item {
|
||||
SpaceItem(
|
||||
selectableSpaceRoom = selectableSpaceRoom,
|
||||
showCheckBox = state.hasOnlyLastAdminRoom.not(),
|
||||
onClick = {
|
||||
state.eventSink(LeaveSpaceEvents.ToggleRoomSelection(selectableSpaceRoom.spaceRoom.roomId))
|
||||
}
|
||||
)
|
||||
if (state.isLastAdmin.not()) {
|
||||
when (state.selectableSpaceRooms) {
|
||||
is AsyncData.Success -> {
|
||||
// List rooms where the user is the only admin
|
||||
state.selectableSpaceRooms.data.forEach { selectableSpaceRoom ->
|
||||
item {
|
||||
SpaceItem(
|
||||
selectableSpaceRoom = selectableSpaceRoom,
|
||||
showCheckBox = state.hasOnlyLastAdminRoom.not(),
|
||||
onClick = {
|
||||
state.eventSink(LeaveSpaceEvents.ToggleRoomSelection(selectableSpaceRoom.spaceRoom.roomId))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is AsyncData.Failure -> item {
|
||||
AsyncFailure(
|
||||
throwable = state.selectableSpaceRooms.error,
|
||||
onRetry = null,
|
||||
)
|
||||
}
|
||||
is AsyncData.Loading,
|
||||
AsyncData.Uninitialized -> item {
|
||||
AsyncLoading()
|
||||
is AsyncData.Failure -> item {
|
||||
AsyncFailure(
|
||||
throwable = state.selectableSpaceRooms.error,
|
||||
onRetry = {
|
||||
state.eventSink(LeaveSpaceEvents.Retry)
|
||||
},
|
||||
)
|
||||
}
|
||||
is AsyncData.Loading,
|
||||
AsyncData.Uninitialized -> item {
|
||||
AsyncLoading()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
LeaveSpaceButtons(
|
||||
showLeaveButton = state.selectableSpaceRooms is AsyncData.Success,
|
||||
showLeaveButton = state.showLeaveButton,
|
||||
selectedRoomsCount = state.selectedRoomsCount,
|
||||
onLeaveSpace = {
|
||||
state.eventSink(LeaveSpaceEvents.LeaveSpace)
|
||||
@@ -130,6 +137,7 @@ fun LeaveSpaceView(
|
||||
AsyncActionView(
|
||||
async = state.leaveSpaceAction,
|
||||
onSuccess = { /* Nothing to do, the screen will be dismissed automatically */ },
|
||||
errorMessage = { stringResource(CommonStrings.error_unknown) },
|
||||
onErrorDismiss = { state.eventSink(LeaveSpaceEvents.CloseError) },
|
||||
)
|
||||
}
|
||||
@@ -150,11 +158,13 @@ private fun LeaveSpaceHeader(
|
||||
modifier = Modifier.padding(top = 0.dp, bottom = 8.dp, start = 24.dp, end = 24.dp),
|
||||
iconStyle = BigIcon.Style.AlertSolid,
|
||||
title = stringResource(
|
||||
R.string.screen_leave_space_title,
|
||||
if (state.isLastAdmin) R.string.screen_leave_space_title_last_admin else R.string.screen_leave_space_title,
|
||||
state.spaceName ?: stringResource(CommonStrings.common_space)
|
||||
),
|
||||
subTitle =
|
||||
if (state.selectableSpaceRooms is AsyncData.Success && state.selectableSpaceRooms.data.isNotEmpty()) {
|
||||
if (state.isLastAdmin) {
|
||||
stringResource(R.string.screen_leave_space_subtitle_last_admin)
|
||||
} else if (state.selectableSpaceRooms is AsyncData.Success && state.selectableSpaceRooms.data.isNotEmpty()) {
|
||||
if (state.hasOnlyLastAdminRoom) {
|
||||
stringResource(R.string.screen_leave_space_subtitle_only_last_admin)
|
||||
} else {
|
||||
@@ -166,34 +176,35 @@ private fun LeaveSpaceHeader(
|
||||
)
|
||||
if (state.showQuickAction) {
|
||||
if (state.areAllSelected) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.align(Alignment.End)
|
||||
.clickable {
|
||||
state.eventSink(LeaveSpaceEvents.DeselectAllRooms)
|
||||
}
|
||||
.padding(vertical = 8.dp, horizontal = 8.dp),
|
||||
text = stringResource(CommonStrings.common_deselect_all),
|
||||
color = ElementTheme.colors.textActionPrimary,
|
||||
style = ElementTheme.typography.fontBodyMdMedium,
|
||||
)
|
||||
QuickActionButton(CommonStrings.common_deselect_all) {
|
||||
state.eventSink(LeaveSpaceEvents.DeselectAllRooms)
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.align(Alignment.End)
|
||||
.clickable {
|
||||
state.eventSink(LeaveSpaceEvents.SelectAllRooms)
|
||||
}
|
||||
.padding(vertical = 8.dp, horizontal = 8.dp),
|
||||
text = stringResource(CommonStrings.common_select_all),
|
||||
color = ElementTheme.colors.textActionPrimary,
|
||||
style = ElementTheme.typography.fontBodyMdMedium,
|
||||
)
|
||||
QuickActionButton(resId = CommonStrings.common_select_all) {
|
||||
state.eventSink(LeaveSpaceEvents.SelectAllRooms)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.QuickActionButton(
|
||||
@StringRes resId: Int,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.align(Alignment.End)
|
||||
.padding(end = 8.dp)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(8.dp),
|
||||
text = stringResource(resId),
|
||||
color = ElementTheme.colors.textActionPrimary,
|
||||
style = ElementTheme.typography.fontBodyMdMedium,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LeaveSpaceButtons(
|
||||
showLeaveButton: Boolean,
|
||||
@@ -202,7 +213,7 @@ private fun LeaveSpaceButtons(
|
||||
onCancel: () -> Unit,
|
||||
) {
|
||||
ButtonColumnMolecule(
|
||||
modifier = Modifier.padding(top = 16.dp)
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
if (showLeaveButton) {
|
||||
val text = if (selectedRoomsCount > 0) {
|
||||
@@ -218,6 +229,8 @@ private fun LeaveSpaceButtons(
|
||||
destructive = true,
|
||||
)
|
||||
}
|
||||
// TODO For least admin space, add a button to open the settings.
|
||||
// See https://www.figma.com/design/kcnHxunG1LDWXsJhaNuiHz/ER-145--Spaces-on-Element-X?node-id=4622-59600
|
||||
TextButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResource(CommonStrings.action_cancel),
|
||||
@@ -300,18 +313,15 @@ private fun SpaceItem(
|
||||
)
|
||||
}
|
||||
// Number of members
|
||||
val subTitle = buildString {
|
||||
append(
|
||||
pluralStringResource(
|
||||
CommonPlurals.common_member_count,
|
||||
room.numJoinedMembers,
|
||||
room.numJoinedMembers
|
||||
)
|
||||
)
|
||||
if (selectableSpaceRoom.isLastAdmin) {
|
||||
append(" ")
|
||||
append(stringResource(R.string.screen_leave_space_last_admin_info))
|
||||
}
|
||||
val membersCount = pluralStringResource(
|
||||
CommonPlurals.common_member_count,
|
||||
room.numJoinedMembers,
|
||||
room.numJoinedMembers
|
||||
)
|
||||
val subTitle = if (selectableSpaceRoom.isLastAdmin) {
|
||||
stringResource(R.string.screen_leave_space_last_admin_info, membersCount)
|
||||
} else {
|
||||
membersCount
|
||||
}
|
||||
Text(
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
|
||||
@@ -202,7 +202,7 @@ private fun LoadingMoreIndicator(
|
||||
private fun SpaceViewTopBar(
|
||||
currentSpace: SpaceRoom?,
|
||||
onBackClick: () -> Unit,
|
||||
@Suppress("unused") onLeaveSpaceClick: () -> Unit,
|
||||
onLeaveSpaceClick: () -> Unit,
|
||||
onShareSpace: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -247,23 +247,25 @@ private fun SpaceViewTopBar(
|
||||
)
|
||||
}
|
||||
)
|
||||
/*
|
||||
// TODO re-enable when we have SDK APIs to leave a space
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
showMenu = false
|
||||
onLeaveSpaceClick()
|
||||
},
|
||||
text = { Text(stringResource(id = CommonStrings.action_leave)) },
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(id = CommonStrings.action_leave),
|
||||
color = ElementTheme.colors.textCriticalPrimary,
|
||||
)
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Leave(),
|
||||
tint = ElementTheme.colors.iconSecondary,
|
||||
tint = ElementTheme.colors.iconCriticalPrimary,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
)
|
||||
*/
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_leave_space_last_admin_info">"(Správce)"</string>
|
||||
<plurals name="screen_leave_space_submit">
|
||||
<item quantity="one">"Opustit %1$d místnost a prostor"</item>
|
||||
<item quantity="few">"Opustit %1$d místnosti a prostor"</item>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_leave_space_last_admin_info">"Administrator"</string>
|
||||
<plurals name="screen_leave_space_submit">
|
||||
<item quantity="one">"Forlad %1$d rum og klynge"</item>
|
||||
<item quantity="other">"Forlad %1$d rum og klynger"</item>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_leave_space_last_admin_info">"(Admin)"</string>
|
||||
<plurals name="screen_leave_space_submit">
|
||||
<item quantity="one">"%1$d Chat und Space verlassen"</item>
|
||||
<item quantity="other">"%1$d Chats und Space verlassen"</item>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_leave_space_last_admin_info">"(Admin)"</string>
|
||||
<plurals name="screen_leave_space_submit">
|
||||
<item quantity="one">"Quitter %1$d salon et l’espace"</item>
|
||||
<item quantity="other">"Quitter %1$d salons et l’espace"</item>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_leave_space_last_admin_info">"(Adminisztrátor)"</string>
|
||||
<plurals name="screen_leave_space_submit">
|
||||
<item quantity="one">"%1$d szoba és tér elhagyása"</item>
|
||||
<item quantity="other">"%1$d szoba és tér elhagyása"</item>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_leave_space_last_admin_info">"(Admin)"</string>
|
||||
<string name="screen_leave_space_last_admin_info">"%1$s (Admin)"</string>
|
||||
<plurals name="screen_leave_space_submit">
|
||||
<item quantity="one">"Leave %1$d room and space"</item>
|
||||
<item quantity="other">"Leave %1$d rooms and space"</item>
|
||||
</plurals>
|
||||
<string name="screen_leave_space_subtitle">"Select the rooms you’d like to leave which you\'re not the only administrator for:"</string>
|
||||
<string name="screen_leave_space_subtitle_last_admin">"You need to assign another admin for this space before you can leave."</string>
|
||||
<string name="screen_leave_space_subtitle_only_last_admin">"You will not be removed from the following room(s) because you\'re the only administrator:"</string>
|
||||
<string name="screen_leave_space_title">"Leave %1$s?"</string>
|
||||
<string name="screen_leave_space_title_last_admin">"You are the only admin for %1$s"</string>
|
||||
</resources>
|
||||
|
||||
@@ -5,60 +5,203 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.features.space.impl.leave
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.spaces.LeaveSpaceHandle
|
||||
import io.element.android.libraries.matrix.api.spaces.LeaveSpaceRoom
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_SPACE_ID
|
||||
import io.element.android.libraries.matrix.test.A_SPACE_NAME
|
||||
import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList
|
||||
import io.element.android.libraries.matrix.test.spaces.FakeLeaveSpaceHandle
|
||||
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import io.element.android.tests.testutils.test
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class LeaveSpacePresenterTest {
|
||||
private val aSpace = aSpaceRoom(
|
||||
roomId = A_SPACE_ID,
|
||||
name = A_SPACE_NAME,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = createLeaveSpacePresenter()
|
||||
val presenter = createLeaveSpacePresenter(
|
||||
leaveSpaceHandle = FakeLeaveSpaceHandle(
|
||||
roomsResult = { Result.success(emptyList()) },
|
||||
),
|
||||
)
|
||||
presenter.test {
|
||||
val state = awaitItem()
|
||||
assertThat(state.spaceName).isNull()
|
||||
assertThat(state.selectableSpaceRooms).isEqualTo(AsyncData.Uninitialized)
|
||||
assertThat(state.isLastAdmin).isFalse()
|
||||
assertThat(state.selectableSpaceRooms.isLoading()).isTrue()
|
||||
assertThat(state.leaveSpaceAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
skipItems(1)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - current space name`() = runTest {
|
||||
val fakeSpaceRoomList = FakeSpaceRoomList()
|
||||
fun `present - fail to load rooms`() = runTest {
|
||||
val presenter = createLeaveSpacePresenter(
|
||||
spaceRoomList = fakeSpaceRoomList,
|
||||
leaveSpaceHandle = FakeLeaveSpaceHandle(
|
||||
roomsResult = { Result.failure(AN_EXCEPTION) },
|
||||
)
|
||||
)
|
||||
presenter.test {
|
||||
val state = awaitItem()
|
||||
advanceUntilIdle()
|
||||
assertThat(state.spaceName).isNull()
|
||||
val aSpace = aSpaceRoom(
|
||||
name = A_SPACE_NAME
|
||||
)
|
||||
fakeSpaceRoomList.emitCurrentSpace(aSpace)
|
||||
assertThat(state.selectableSpaceRooms.isLoading()).isTrue()
|
||||
assertThat(state.leaveSpaceAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
skipItems(3)
|
||||
val stateError = awaitItem()
|
||||
assertThat(stateError.selectableSpaceRooms.isFailure()).isTrue()
|
||||
// Retry
|
||||
stateError.eventSink(LeaveSpaceEvents.Retry)
|
||||
skipItems(1)
|
||||
assertThat(awaitItem().spaceName).isEqualTo(A_SPACE_NAME)
|
||||
val stateLoadingAgain = awaitItem()
|
||||
assertThat(stateLoadingAgain.selectableSpaceRooms.isLoading()).isTrue()
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - current space name and is last admin`() = runTest {
|
||||
val presenter = createLeaveSpacePresenter(
|
||||
leaveSpaceHandle = FakeLeaveSpaceHandle(
|
||||
roomsResult = { Result.success(listOf(aLeaveSpaceRoom(spaceRoom = aSpace, isLastAdmin = true))) },
|
||||
)
|
||||
)
|
||||
presenter.test {
|
||||
val state = awaitItem()
|
||||
assertThat(state.spaceName).isNull()
|
||||
skipItems(3)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.spaceName).isEqualTo(A_SPACE_NAME)
|
||||
assertThat(finalState.isLastAdmin).isTrue()
|
||||
// The current state is not in the sub room list
|
||||
assertThat(finalState.selectableSpaceRooms.dataOrNull()!!).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - leave space and sub rooms`() = runTest {
|
||||
val leaveResult = lambdaRecorder<List<RoomId>, Result<Unit>> { Result.success(Unit) }
|
||||
val presenter = createLeaveSpacePresenter(
|
||||
leaveSpaceHandle = FakeLeaveSpaceHandle(
|
||||
roomsResult = {
|
||||
Result.success(
|
||||
listOf(
|
||||
LeaveSpaceRoom(aSpaceRoom(roomId = A_ROOM_ID), isLastAdmin = false),
|
||||
LeaveSpaceRoom(aSpaceRoom(roomId = A_ROOM_ID_2), isLastAdmin = true),
|
||||
)
|
||||
)
|
||||
},
|
||||
leaveResult = leaveResult,
|
||||
)
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(4)
|
||||
val state = awaitItem()
|
||||
assertThat(state.spaceName).isNull()
|
||||
assertThat(state.isLastAdmin).isFalse()
|
||||
val data = state.selectableSpaceRooms.dataOrNull()!!
|
||||
assertThat(data.size).isEqualTo(2)
|
||||
// Only one room is selectable as the user is the last admin in the other one
|
||||
val room1 = data[0]
|
||||
assertThat(room1.spaceRoom.roomId).isEqualTo(A_ROOM_ID)
|
||||
assertThat(room1.isSelected).isTrue()
|
||||
assertThat(room1.isLastAdmin).isFalse()
|
||||
val room2 = data[1]
|
||||
assertThat(room2.spaceRoom.roomId).isEqualTo(A_ROOM_ID_2)
|
||||
assertThat(room2.isSelected).isFalse()
|
||||
assertThat(room2.isLastAdmin).isTrue()
|
||||
// Deselect all
|
||||
state.eventSink(LeaveSpaceEvents.DeselectAllRooms)
|
||||
skipItems(1)
|
||||
val stateAllDeselected = awaitItem()
|
||||
val dataAllDeselected = stateAllDeselected.selectableSpaceRooms.dataOrNull()!!
|
||||
assertThat(dataAllDeselected.any { it.isSelected }).isFalse()
|
||||
// Select all
|
||||
stateAllDeselected.eventSink(LeaveSpaceEvents.SelectAllRooms)
|
||||
skipItems(1)
|
||||
val stateAllSelected = awaitItem()
|
||||
val dataAllSelected = stateAllSelected.selectableSpaceRooms.dataOrNull()!!
|
||||
// The last admin room should not be selected
|
||||
assertThat(dataAllSelected.count { it.isSelected }).isEqualTo(1)
|
||||
// Toggle selection of the first room
|
||||
stateAllSelected.eventSink(LeaveSpaceEvents.ToggleRoomSelection(A_ROOM_ID))
|
||||
skipItems(1)
|
||||
val stateOneDeselected = awaitItem()
|
||||
val dataOneDeselected = stateOneDeselected.selectableSpaceRooms.dataOrNull()!!
|
||||
assertThat(dataOneDeselected[0].isSelected).isFalse()
|
||||
// Toggle selection of the first room
|
||||
stateOneDeselected.eventSink(LeaveSpaceEvents.ToggleRoomSelection(A_ROOM_ID))
|
||||
skipItems(1)
|
||||
val stateOneSelected = awaitItem()
|
||||
val dataOneSelected = stateOneSelected.selectableSpaceRooms.dataOrNull()!!
|
||||
assertThat(dataOneSelected[0].isSelected).isTrue()
|
||||
// Leave space
|
||||
stateOneSelected.eventSink(LeaveSpaceEvents.LeaveSpace)
|
||||
val stateLeaving = awaitItem()
|
||||
assertThat(stateLeaving.leaveSpaceAction).isEqualTo(AsyncAction.Loading)
|
||||
val stateLeft = awaitItem()
|
||||
assertThat(stateLeft.leaveSpaceAction.isSuccess()).isTrue()
|
||||
leaveResult.assertions().isCalledOnce().with(
|
||||
value(listOf(A_ROOM_ID))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - leave space error and close`() = runTest {
|
||||
val leaveResult = lambdaRecorder<List<RoomId>, Result<Unit>> {
|
||||
Result.failure(AN_EXCEPTION)
|
||||
}
|
||||
val presenter = createLeaveSpacePresenter(
|
||||
leaveSpaceHandle = FakeLeaveSpaceHandle(
|
||||
roomsResult = { Result.success(emptyList()) },
|
||||
leaveResult = leaveResult,
|
||||
)
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(4)
|
||||
val state = awaitItem()
|
||||
state.eventSink(LeaveSpaceEvents.LeaveSpace)
|
||||
val stateLeaving = awaitItem()
|
||||
assertThat(stateLeaving.leaveSpaceAction).isEqualTo(AsyncAction.Loading)
|
||||
val stateError = awaitItem()
|
||||
assertThat(stateError.leaveSpaceAction.isFailure()).isTrue()
|
||||
// Close error
|
||||
stateError.eventSink(LeaveSpaceEvents.CloseError)
|
||||
val stateErrorClosed = awaitItem()
|
||||
assertThat(stateErrorClosed.leaveSpaceAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createLeaveSpacePresenter(
|
||||
spaceRoomList: SpaceRoomList = FakeSpaceRoomList(),
|
||||
leaveSpaceHandle: LeaveSpaceHandle = FakeLeaveSpaceHandle(),
|
||||
): LeaveSpacePresenter {
|
||||
return LeaveSpacePresenter(
|
||||
spaceRoomList = spaceRoomList,
|
||||
leaveSpaceHandle = leaveSpaceHandle,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun aLeaveSpaceRoom(
|
||||
spaceRoom: SpaceRoom = aSpaceRoom(
|
||||
roomId = A_SPACE_ID,
|
||||
name = A_SPACE_NAME,
|
||||
),
|
||||
isLastAdmin: Boolean = false,
|
||||
) = LeaveSpaceRoom(
|
||||
spaceRoom = spaceRoom,
|
||||
isLastAdmin = isLastAdmin,
|
||||
)
|
||||
|
||||
@@ -20,6 +20,7 @@ class LeaveSpaceStateTest {
|
||||
selectableSpaceRooms = AsyncData.Loading()
|
||||
)
|
||||
assertThat(sut.showQuickAction).isFalse()
|
||||
assertThat(sut.showLeaveButton).isFalse()
|
||||
assertThat(sut.areAllSelected).isTrue()
|
||||
assertThat(sut.hasOnlyLastAdminRoom).isFalse()
|
||||
assertThat(sut.selectedRoomsCount).isEqualTo(0)
|
||||
@@ -33,11 +34,29 @@ class LeaveSpaceStateTest {
|
||||
)
|
||||
)
|
||||
assertThat(sut.showQuickAction).isFalse()
|
||||
assertThat(sut.showLeaveButton).isTrue()
|
||||
assertThat(sut.areAllSelected).isTrue()
|
||||
assertThat(sut.hasOnlyLastAdminRoom).isFalse()
|
||||
assertThat(sut.selectedRoomsCount).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test last admin`() {
|
||||
val sut = aLeaveSpaceState(
|
||||
isLastAdmin = true,
|
||||
selectableSpaceRooms = AsyncData.Success(
|
||||
persistentListOf(
|
||||
aSelectableSpaceRoom(isLastAdmin = false, isSelected = false),
|
||||
)
|
||||
)
|
||||
)
|
||||
assertThat(sut.showQuickAction).isFalse()
|
||||
assertThat(sut.showLeaveButton).isFalse()
|
||||
assertThat(sut.areAllSelected).isFalse()
|
||||
assertThat(sut.hasOnlyLastAdminRoom).isFalse()
|
||||
assertThat(sut.selectedRoomsCount).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test no last admin, 1 selected, 1 not selected`() {
|
||||
val sut = aLeaveSpaceState(
|
||||
@@ -49,6 +68,7 @@ class LeaveSpaceStateTest {
|
||||
)
|
||||
)
|
||||
assertThat(sut.showQuickAction).isTrue()
|
||||
assertThat(sut.showLeaveButton).isTrue()
|
||||
assertThat(sut.areAllSelected).isFalse()
|
||||
assertThat(sut.hasOnlyLastAdminRoom).isFalse()
|
||||
assertThat(sut.selectedRoomsCount).isEqualTo(1)
|
||||
@@ -65,6 +85,7 @@ class LeaveSpaceStateTest {
|
||||
)
|
||||
)
|
||||
assertThat(sut.showQuickAction).isTrue()
|
||||
assertThat(sut.showLeaveButton).isTrue()
|
||||
assertThat(sut.areAllSelected).isTrue()
|
||||
assertThat(sut.hasOnlyLastAdminRoom).isFalse()
|
||||
assertThat(sut.selectedRoomsCount).isEqualTo(2)
|
||||
@@ -82,6 +103,7 @@ class LeaveSpaceStateTest {
|
||||
)
|
||||
)
|
||||
assertThat(sut.showQuickAction).isTrue()
|
||||
assertThat(sut.showLeaveButton).isTrue()
|
||||
assertThat(sut.areAllSelected).isTrue()
|
||||
assertThat(sut.hasOnlyLastAdminRoom).isFalse()
|
||||
assertThat(sut.selectedRoomsCount).isEqualTo(2)
|
||||
@@ -98,6 +120,7 @@ class LeaveSpaceStateTest {
|
||||
)
|
||||
)
|
||||
assertThat(sut.showQuickAction).isFalse()
|
||||
assertThat(sut.showLeaveButton).isTrue()
|
||||
assertThat(sut.areAllSelected).isTrue()
|
||||
assertThat(sut.hasOnlyLastAdminRoom).isTrue()
|
||||
assertThat(sut.selectedRoomsCount).isEqualTo(0)
|
||||
|
||||
@@ -166,7 +166,7 @@ test_detekt_test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version
|
||||
# https://github.com/matrix-org/matrix-rust-components-kotlin/commits/main/sdk/sdk-android/src/main/kotlin/org/matrix/rustcomponents/sdk/matrix_sdk_ffi.kt
|
||||
# All new features should not be implemented in the pull request that upgrades the version, developers should
|
||||
# only fix API breaks and may add some TODOs.
|
||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.10.1"
|
||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.10.2"
|
||||
|
||||
# Others
|
||||
coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }
|
||||
|
||||
@@ -69,14 +69,6 @@ object MatrixPatterns {
|
||||
str matches PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if a string is a valid space id. This is an alias for [isRoomId]
|
||||
*
|
||||
* @param str the string to test
|
||||
* @return true if the string is a valid space Id
|
||||
*/
|
||||
fun isSpaceId(str: String?) = isRoomId(str)
|
||||
|
||||
/**
|
||||
* Tells if a string is a valid room id.
|
||||
*
|
||||
|
||||
@@ -7,23 +7,7 @@
|
||||
|
||||
package io.element.android.libraries.matrix.api.core
|
||||
|
||||
import io.element.android.libraries.androidutils.metadata.isInDebug
|
||||
import java.io.Serializable
|
||||
|
||||
@JvmInline
|
||||
value class SpaceId(val value: String) : Serializable {
|
||||
init {
|
||||
if (isInDebug && !MatrixPatterns.isSpaceId(value)) {
|
||||
error(
|
||||
"`$value` is not a valid space id.\n" +
|
||||
"Space ids are the same as room ids.\n" +
|
||||
"Example space id: `!space_id:domain`."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String = value
|
||||
}
|
||||
typealias SpaceId = RoomId
|
||||
|
||||
/**
|
||||
* Value to use when no space is selected by the user.
|
||||
|
||||
@@ -11,6 +11,7 @@ import io.element.android.libraries.matrix.api.exception.ClientException
|
||||
|
||||
sealed class RecoveryException(message: String) : Exception(message) {
|
||||
class SecretStorage(message: String) : RecoveryException(message)
|
||||
class Import(message: String) : RecoveryException(message)
|
||||
data object BackupExistsOnServer : RecoveryException("BackupExistsOnServer")
|
||||
data class Client(val exception: ClientException) : RecoveryException(exception.message ?: "Unknown error")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.libraries.matrix.api.spaces
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
interface LeaveSpaceHandle {
|
||||
/**
|
||||
* The id of the space to leave.
|
||||
*/
|
||||
val id: RoomId
|
||||
|
||||
/**
|
||||
* Get a list of rooms that can be left when leaving the space.
|
||||
* It will include the current space and all the subspaces and rooms that the user has joined.
|
||||
*/
|
||||
suspend fun rooms(): Result<List<LeaveSpaceRoom>>
|
||||
|
||||
/**
|
||||
* Leave the space and the given rooms.
|
||||
* If [roomIds] is empty, only the space will be left.
|
||||
*/
|
||||
suspend fun leave(roomIds: List<RoomId>): Result<Unit>
|
||||
|
||||
/**
|
||||
* Close the handle and free resources.
|
||||
*/
|
||||
fun close()
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* 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.libraries.matrix.api.spaces
|
||||
|
||||
data class LeaveSpaceRoom(
|
||||
val spaceRoom: SpaceRoom,
|
||||
val isLastAdmin: Boolean,
|
||||
)
|
||||
@@ -15,4 +15,6 @@ interface SpaceService {
|
||||
suspend fun joinedSpaces(): Result<List<SpaceRoom>>
|
||||
|
||||
fun spaceRoomList(id: RoomId): SpaceRoomList
|
||||
|
||||
fun getLeaveSpaceHandle(spaceId: RoomId): LeaveSpaceHandle
|
||||
}
|
||||
|
||||
@@ -147,6 +147,8 @@ class RustMatrixClient(
|
||||
private val innerRoomListService = innerSyncService.roomListService()
|
||||
private val innerSpaceService = innerClient.spaceService()
|
||||
|
||||
private val roomMembershipObserver = RoomMembershipObserver()
|
||||
|
||||
private val rustSyncService = RustSyncService(
|
||||
inner = innerSyncService,
|
||||
dispatcher = sessionDispatcher,
|
||||
@@ -189,6 +191,7 @@ class RustMatrixClient(
|
||||
|
||||
override val spaceService: SpaceService = RustSpaceService(
|
||||
innerSpaceService = innerSpaceService,
|
||||
roomMembershipObserver = roomMembershipObserver,
|
||||
sessionCoroutineScope = sessionCoroutineScope,
|
||||
sessionDispatcher = sessionDispatcher,
|
||||
)
|
||||
@@ -200,7 +203,7 @@ class RustMatrixClient(
|
||||
)
|
||||
|
||||
private val roomInfoMapper = RoomInfoMapper()
|
||||
private val roomMembershipObserver = RoomMembershipObserver()
|
||||
|
||||
private val roomFactory = RustRoomFactory(
|
||||
roomListService = roomListService,
|
||||
innerRoomListService = innerRoomListService,
|
||||
|
||||
@@ -20,6 +20,9 @@ fun Throwable.mapRecoveryException(): RecoveryException {
|
||||
message = errorMessage
|
||||
)
|
||||
is RustRecoveryException.BackupExistsOnServer -> RecoveryException.BackupExistsOnServer
|
||||
is RustRecoveryException.Import -> RecoveryException.Import(
|
||||
message = errorMessage
|
||||
)
|
||||
is RustRecoveryException.Client -> RecoveryException.Client(
|
||||
source.mapClientException()
|
||||
)
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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.libraries.matrix.impl.spaces
|
||||
|
||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.spaces.LeaveSpaceHandle
|
||||
import io.element.android.libraries.matrix.api.spaces.LeaveSpaceRoom
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import org.matrix.rustcomponents.sdk.LeaveSpaceHandle as RustLeaveSpaceHandle
|
||||
|
||||
class RustLeaveSpaceHandle(
|
||||
override val id: RoomId,
|
||||
private val spaceRoomMapper: SpaceRoomMapper,
|
||||
private val roomMembershipObserver: RoomMembershipObserver,
|
||||
sessionCoroutineScope: CoroutineScope,
|
||||
private val innerProvider: suspend () -> RustLeaveSpaceHandle,
|
||||
) : LeaveSpaceHandle {
|
||||
private val inner = CompletableDeferred<RustLeaveSpaceHandle>()
|
||||
|
||||
init {
|
||||
sessionCoroutineScope.launch {
|
||||
inner.complete(innerProvider())
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun rooms(): Result<List<LeaveSpaceRoom>> = runCatchingExceptions {
|
||||
inner.await().rooms().map { leaveSpaceRoom ->
|
||||
LeaveSpaceRoom(
|
||||
spaceRoom = spaceRoomMapper.map(leaveSpaceRoom.spaceRoom),
|
||||
isLastAdmin = leaveSpaceRoom.isLastAdmin,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun leave(roomIds: List<RoomId>): Result<Unit> = runCatchingExceptions {
|
||||
// Ensure the space is included and is the last room to be left
|
||||
val roomToLeave = roomIds - id + id
|
||||
inner.await().leave(roomToLeave.map { it.value })
|
||||
}.onSuccess {
|
||||
roomMembershipObserver.notifyUserLeftRoom(
|
||||
roomId = id,
|
||||
isSpace = true,
|
||||
membershipBeforeLeft = CurrentUserMembership.JOINED,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun close() {
|
||||
Timber.d("Destroying LeaveSpaceHandle $id")
|
||||
try {
|
||||
inner.getCompleted().destroy()
|
||||
} catch (_: Exception) {
|
||||
// Ignore, we just want to make sure it's completed
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ package io.element.android.libraries.matrix.impl.spaces
|
||||
import io.element.android.libraries.core.coroutine.childScope
|
||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.spaces.LeaveSpaceHandle
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceService
|
||||
@@ -37,6 +39,7 @@ class RustSpaceService(
|
||||
private val innerSpaceService: ClientSpaceService,
|
||||
private val sessionCoroutineScope: CoroutineScope,
|
||||
private val sessionDispatcher: CoroutineDispatcher,
|
||||
private val roomMembershipObserver: RoomMembershipObserver,
|
||||
) : SpaceService {
|
||||
private val spaceRoomMapper = SpaceRoomMapper()
|
||||
override val spaceRoomsFlow = MutableSharedFlow<List<SpaceRoom>>(replay = 1, extraBufferCapacity = 1)
|
||||
@@ -64,6 +67,17 @@ class RustSpaceService(
|
||||
)
|
||||
}
|
||||
|
||||
override fun getLeaveSpaceHandle(spaceId: RoomId): LeaveSpaceHandle {
|
||||
return RustLeaveSpaceHandle(
|
||||
id = spaceId,
|
||||
spaceRoomMapper = spaceRoomMapper,
|
||||
roomMembershipObserver = roomMembershipObserver,
|
||||
sessionCoroutineScope = sessionCoroutineScope,
|
||||
) {
|
||||
innerSpaceService.leaveSpace(spaceId.value)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
innerSpaceService
|
||||
.spaceListUpdate()
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.libraries.matrix.test.spaces
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.spaces.LeaveSpaceHandle
|
||||
import io.element.android.libraries.matrix.api.spaces.LeaveSpaceRoom
|
||||
import io.element.android.libraries.matrix.test.A_SPACE_ID
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.simulateLongTask
|
||||
|
||||
class FakeLeaveSpaceHandle(
|
||||
override val id: RoomId = A_SPACE_ID,
|
||||
private val roomsResult: () -> Result<List<LeaveSpaceRoom>> = { lambdaError() },
|
||||
private val leaveResult: (List<RoomId>) -> Result<Unit> = { lambdaError() },
|
||||
private val closeResult: () -> Unit = { lambdaError() },
|
||||
) : LeaveSpaceHandle {
|
||||
override suspend fun rooms(): Result<List<LeaveSpaceRoom>> = simulateLongTask {
|
||||
roomsResult()
|
||||
}
|
||||
|
||||
override suspend fun leave(roomIds: List<RoomId>): Result<Unit> = simulateLongTask {
|
||||
leaveResult(roomIds)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
return closeResult()
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
package io.element.android.libraries.matrix.test.spaces
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.spaces.LeaveSpaceHandle
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceService
|
||||
@@ -20,6 +21,7 @@ import kotlinx.coroutines.flow.asSharedFlow
|
||||
class FakeSpaceService(
|
||||
private val joinedSpacesResult: () -> Result<List<SpaceRoom>> = { lambdaError() },
|
||||
private val spaceRoomListResult: (RoomId) -> SpaceRoomList = { lambdaError() },
|
||||
private val leaveSpaceHandleResult: (RoomId) -> LeaveSpaceHandle = { lambdaError() },
|
||||
) : SpaceService {
|
||||
private val _spaceRoomsFlow = MutableSharedFlow<List<SpaceRoom>>()
|
||||
override val spaceRoomsFlow: SharedFlow<List<SpaceRoom>>
|
||||
@@ -36,4 +38,8 @@ class FakeSpaceService(
|
||||
override fun spaceRoomList(id: RoomId): SpaceRoomList {
|
||||
return spaceRoomListResult(id)
|
||||
}
|
||||
|
||||
override fun getLeaveSpaceHandle(spaceId: RoomId): LeaveSpaceHandle {
|
||||
return leaveSpaceHandleResult(spaceId)
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user