Merge pull request #5432 from element-hq/feature/bma/leaveSpace
Leave space: use SDK API.
This commit is contained in:
commit
0c63d0cb4f
48 changed files with 602 additions and 210 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5219016a0af4e1e4f7703dcef3400bf030444845dc0ef52f084e69963170bf1e
|
||||
size 13951
|
||||
oid sha256:3c569a42e638a49554127d3dc59c1dda5306e50406a6eaf1033345b44e2fe037
|
||||
size 13941
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:964aff2fc49b9d71ebec506bc9acc5010071efca01ed95f240a65b2c91f0f3b1
|
||||
size 15852
|
||||
oid sha256:b43c73a9da70133fdff8c63dd6ed8b130b76fbbc28977fae9a34733b0f64faea
|
||||
size 15840
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a2097486621a733b1662c02849ec71423d00f9c97c679996fafc8f20a4ef27ce
|
||||
size 43844
|
||||
oid sha256:02ea4a02c0901c3d31a86272659f251e3b9299f669177b7a46a9007159989519
|
||||
size 44313
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:daff7333af912c8a0d7f009ba10d044aaa7cdfea2dfa3ccff2fd97209bf3278e
|
||||
size 44225
|
||||
oid sha256:0a8d851c9393b9a64642d08be96b0c449ce894fdf69d52d0eb664d83c7005520
|
||||
size 44211
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:aa05867ac79a3bfb9f59bf1dafb02185274b46e9a5fbf27415a0e907780a0493
|
||||
size 36393
|
||||
oid sha256:78cc2ef488b13f72da4d1d149329930816e1d91541bbe62eb48c78b61564dc18
|
||||
size 35868
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e74abc389c25fa9a5d33ade6d0f8a2242aeb5150fe81a26e26e9b561c46bb802
|
||||
size 43010
|
||||
oid sha256:426847168cf1cf81df60193ba69d1c02cad567f0a72c03f0b2dadb76e92f41c0
|
||||
size 42600
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b5c8bdf8d389cccb4cced29aa854a70860e9b44cbdde2a25ab95ccd5bc5e1674
|
||||
size 39222
|
||||
oid sha256:479fb84a857b3fcb15a9bba6a6fac3f8e1309a7d5af9c10c1cfe1d16c2eb678f
|
||||
size 42208
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bbf5d7c5320daa0d6b3c6678a6766c7143bc877850257e2c6923b174dfa9b6d4
|
||||
size 34565
|
||||
oid sha256:ba7631fe1e5d20946256829e8414f5a305e3a2755d79fc0693fd932bfac40f4a
|
||||
size 40182
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:298db58a3f80f4194c5710ceeabec0e49b0e80861b931693d58b1a17f4394b4e
|
||||
size 13873
|
||||
oid sha256:6d5be43f0ae09dfea01efa28519b003c6c3bbd8a47c8329ccc9721acdc84a116
|
||||
size 16531
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:520698bcbc65cd2ccbc73aff01132a6b5d39c7d349a814e5a26e584cec5f2eeb
|
||||
size 26083
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b982dc465fd47dbce6f88321f80c5390ecc90f7e8ad59a76dd44b6c2b9b80c8a
|
||||
size 13923
|
||||
oid sha256:1935a53ffe3653f9c094996a7b62359fe70c18ea0b46a7204969f70ce439b5e7
|
||||
size 13924
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:00b13582a4a8a380cc502ca2bb181c1ee7c9f2e1685e1d2a75817107d5ebea99
|
||||
size 15393
|
||||
oid sha256:f90882b794619797d17c9bdb8ff2e56664a5bd95ab9bee0bb59371f954c0e7ca
|
||||
size 15395
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6ad6ae058adb3decb4733c74ad40e4efdd33744eea3e9a793e92fc6b4cbc1464
|
||||
size 42784
|
||||
oid sha256:4071cfff533916af1c99636ce6f61e2fbfd8d945dbba1b11e449ce55fd4bd7b9
|
||||
size 43148
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ad30950d847bd769e19ce67d8944c16145e929c52210cd54b1ebc5966efe89b5
|
||||
size 43221
|
||||
oid sha256:03dd230a42399b5db39a57dbde170955c7f40793fced9420e2fcf8c55820b8c4
|
||||
size 42948
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6465f03be41ae02635cbdc94f521059bdd6476c5b2ccfca6f28ccd8898112cea
|
||||
size 35530
|
||||
oid sha256:f8d4a0d5f17cf6781d34af68ec0fae1c64cdf88d59ddf4306db411c4c67fcfd8
|
||||
size 34945
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:af017745220027708f5a84ca639c9990fe7e474a38aaeecc3cba82011d195360
|
||||
size 42080
|
||||
oid sha256:26fcf6cfae843737df553b143d9cbb8049001548b0c3479e1e24a28e0c5e6527
|
||||
size 41632
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:73f85bf9ec6c6682e293db2c740d206f6d895d79b3f3d30b967ad55aedbb268c
|
||||
size 37895
|
||||
oid sha256:bc8a9e1d6fd7a81cf1ef718d2f3e38babcf96e58e00c2c92827cba0aadd8903e
|
||||
size 40613
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:49e2727c42d334c539ee14aab396cadd68b1290e6a256ec2f677f7e4801b30e9
|
||||
size 32677
|
||||
oid sha256:1f0ce9d4bbe5a8bdadee5663e8ad8b9b189aa25b5eec7e7c1a40430c09f7d1d3
|
||||
size 38108
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6ef494178398bc0f8e7721b7dfde669da4d723aeafa0d4ca6975a58215193416
|
||||
size 13848
|
||||
oid sha256:16d9c0ce7ffefb7825efb5e824c06bc721f85f3ff88340abf9c41b2b4679492c
|
||||
size 16435
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3ac66ddfcf3125726d2a9a7a83ef23880339f56c12bf2cad68bc969a4deb0f38
|
||||
size 25776
|
||||
Loading…
Add table
Add a link
Reference in a new issue