Merge pull request #5432 from element-hq/feature/bma/leaveSpace
Leave space: use SDK API.
This commit is contained in:
commit
ffde95fb84
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
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
sealed interface LeaveSpaceEvents {
|
sealed interface LeaveSpaceEvents {
|
||||||
|
data object Retry : LeaveSpaceEvents
|
||||||
data object SelectAllRooms : LeaveSpaceEvents
|
data object SelectAllRooms : LeaveSpaceEvents
|
||||||
data object DeselectAllRooms : LeaveSpaceEvents
|
data object DeselectAllRooms : LeaveSpaceEvents
|
||||||
data class ToggleRoomSelection(val roomId: RoomId) : 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.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.bumble.appyx.core.lifecycle.subscribe
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.AssistedInject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import io.element.android.annotations.ContributesNode
|
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.features.space.impl.di.SpaceFlowScope
|
||||||
|
import io.element.android.libraries.architecture.inputs
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
|
||||||
@ContributesNode(SpaceFlowScope::class)
|
@ContributesNode(SpaceFlowScope::class)
|
||||||
@AssistedInject
|
@AssistedInject
|
||||||
class LeaveSpaceNode(
|
class LeaveSpaceNode(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
private val presenter: LeaveSpacePresenter,
|
matrixClient: MatrixClient,
|
||||||
|
presenterFactory: LeaveSpacePresenter.Factory,
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : 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
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
|
|
|
||||||
|
|
@ -8,92 +8,119 @@
|
||||||
package io.element.android.features.space.impl.leave
|
package io.element.android.features.space.impl.leave
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.produceState
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
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.AsyncAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.architecture.Presenter
|
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.architecture.runUpdatingState
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
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.LeaveSpaceHandle
|
||||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
import io.element.android.libraries.matrix.api.spaces.LeaveSpaceRoom
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.ImmutableSet
|
|
||||||
import kotlinx.collections.immutable.persistentSetOf
|
import kotlinx.collections.immutable.persistentSetOf
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.collections.immutable.toPersistentSet
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.jvm.optionals.getOrNull
|
|
||||||
|
|
||||||
@Inject
|
@AssistedInject
|
||||||
class LeaveSpacePresenter(
|
class LeaveSpacePresenter(
|
||||||
private val spaceRoomList: SpaceRoomList,
|
@Assisted private val leaveSpaceHandle: LeaveSpaceHandle,
|
||||||
) : Presenter<LeaveSpaceState> {
|
) : Presenter<LeaveSpaceState> {
|
||||||
|
@AssistedFactory
|
||||||
|
fun interface Factory {
|
||||||
|
fun create(leaveSpaceHandle: LeaveSpaceHandle): LeaveSpacePresenter
|
||||||
|
}
|
||||||
|
|
||||||
|
data class LeaveSpaceRooms(
|
||||||
|
val current: LeaveSpaceRoom?,
|
||||||
|
val others: List<LeaveSpaceRoom>,
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): LeaveSpaceState {
|
override fun present(): LeaveSpaceState {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val currentSpace by spaceRoomList.currentSpaceFlow.collectAsState()
|
var retryCount by remember { mutableIntStateOf(0) }
|
||||||
val leaveSpaceAction = remember {
|
val leaveSpaceAction = remember {
|
||||||
mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
|
mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
|
||||||
}
|
}
|
||||||
val selectedRoomIds = remember {
|
var selectedRoomIds by remember {
|
||||||
mutableStateOf<ImmutableSet<RoomId>>(persistentSetOf())
|
mutableStateOf<Collection<RoomId>>(setOf())
|
||||||
}
|
}
|
||||||
val joinedSpaceRooms by produceState(emptyList()) {
|
var leaveSpaceRooms by remember {
|
||||||
// TODO Get the joined room from the SDK, should also have the isLastAdmin boolean
|
mutableStateOf<AsyncData<LeaveSpaceRooms>>(AsyncData.Loading())
|
||||||
val rooms = emptyList<SpaceRoom>()
|
|
||||||
// By default select all rooms
|
|
||||||
selectedRoomIds.value = rooms.map { it.roomId }.toPersistentSet()
|
|
||||||
value = rooms
|
|
||||||
}
|
}
|
||||||
val selectableSpaceRooms by produceState<AsyncData<ImmutableList<SelectableSpaceRoom>>>(
|
LaunchedEffect(retryCount) {
|
||||||
initialValue = AsyncData.Uninitialized,
|
val rooms = leaveSpaceHandle.rooms()
|
||||||
key1 = joinedSpaceRooms,
|
val (currentRoom, otherRooms) = rooms.getOrNull()
|
||||||
key2 = selectedRoomIds.value,
|
.orEmpty()
|
||||||
) {
|
.partition { it.spaceRoom.roomId == leaveSpaceHandle.id }
|
||||||
value = AsyncData.Success(
|
// By default select all rooms that can be left
|
||||||
joinedSpaceRooms.map {
|
selectedRoomIds = otherRooms
|
||||||
SelectableSpaceRoom(
|
.filter { it.isLastAdmin.not() }
|
||||||
spaceRoom = it,
|
.map { it.spaceRoom.roomId }
|
||||||
// TODO Get this value from the SDK
|
leaveSpaceRooms = rooms.fold(
|
||||||
isLastAdmin = false,
|
onSuccess = {
|
||||||
isSelected = selectedRoomIds.value.contains(it.roomId),
|
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) {
|
fun handleEvents(event: LeaveSpaceEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
|
LeaveSpaceEvents.Retry -> {
|
||||||
|
leaveSpaceRooms = AsyncData.Loading()
|
||||||
|
retryCount += 1
|
||||||
|
}
|
||||||
LeaveSpaceEvents.DeselectAllRooms -> {
|
LeaveSpaceEvents.DeselectAllRooms -> {
|
||||||
selectedRoomIds.value = persistentSetOf()
|
selectedRoomIds = persistentSetOf()
|
||||||
}
|
}
|
||||||
LeaveSpaceEvents.SelectAllRooms -> {
|
LeaveSpaceEvents.SelectAllRooms -> {
|
||||||
selectedRoomIds.value = selectableSpaceRooms.dataOrNull()
|
selectedRoomIds = selectableSpaceRooms.dataOrNull()
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
.filter { it.isLastAdmin.not() }
|
.filter { it.isLastAdmin.not() }
|
||||||
.map { it.spaceRoom.roomId }
|
.map { it.spaceRoom.roomId }
|
||||||
.toPersistentSet()
|
|
||||||
}
|
}
|
||||||
is LeaveSpaceEvents.ToggleRoomSelection -> {
|
is LeaveSpaceEvents.ToggleRoomSelection -> {
|
||||||
val currentSet = selectedRoomIds.value
|
selectedRoomIds = if (selectedRoomIds.contains(event.roomId)) {
|
||||||
selectedRoomIds.value = if (currentSet.contains(event.roomId)) {
|
selectedRoomIds - event.roomId
|
||||||
currentSet - event.roomId
|
|
||||||
} else {
|
} else {
|
||||||
currentSet + event.roomId
|
selectedRoomIds + event.roomId
|
||||||
}
|
}
|
||||||
.toPersistentSet()
|
|
||||||
}
|
}
|
||||||
LeaveSpaceEvents.LeaveSpace -> coroutineScope.leaveSpace(
|
LeaveSpaceEvents.LeaveSpace -> coroutineScope.leaveSpace(
|
||||||
leaveSpaceAction = leaveSpaceAction,
|
leaveSpaceAction = leaveSpaceAction,
|
||||||
selectedRoomIds = selectedRoomIds.value,
|
selectedRoomIds = selectedRoomIds,
|
||||||
)
|
)
|
||||||
LeaveSpaceEvents.CloseError -> {
|
LeaveSpaceEvents.CloseError -> {
|
||||||
leaveSpaceAction.value = AsyncAction.Uninitialized
|
leaveSpaceAction.value = AsyncAction.Uninitialized
|
||||||
|
|
@ -102,7 +129,8 @@ class LeaveSpacePresenter(
|
||||||
}
|
}
|
||||||
|
|
||||||
return LeaveSpaceState(
|
return LeaveSpaceState(
|
||||||
spaceName = currentSpace.getOrNull()?.name,
|
spaceName = leaveSpaceRooms.dataOrNull()?.current?.spaceRoom?.name,
|
||||||
|
isLastAdmin = leaveSpaceRooms.dataOrNull()?.current?.isLastAdmin == true,
|
||||||
selectableSpaceRooms = selectableSpaceRooms,
|
selectableSpaceRooms = selectableSpaceRooms,
|
||||||
leaveSpaceAction = leaveSpaceAction.value,
|
leaveSpaceAction = leaveSpaceAction.value,
|
||||||
eventSink = ::handleEvents,
|
eventSink = ::handleEvents,
|
||||||
|
|
@ -111,11 +139,10 @@ class LeaveSpacePresenter(
|
||||||
|
|
||||||
private fun CoroutineScope.leaveSpace(
|
private fun CoroutineScope.leaveSpace(
|
||||||
leaveSpaceAction: MutableState<AsyncAction<Unit>>,
|
leaveSpaceAction: MutableState<AsyncAction<Unit>>,
|
||||||
@Suppress("unused") selectedRoomIds: Set<RoomId>,
|
selectedRoomIds: Collection<RoomId>,
|
||||||
) = launch {
|
) = launch {
|
||||||
runUpdatingState(leaveSpaceAction) {
|
runUpdatingState(leaveSpaceAction) {
|
||||||
// TODO SDK API call to leave all the rooms and space
|
leaveSpaceHandle.leave(selectedRoomIds.toList())
|
||||||
Result.failure(Exception("Not implemented"))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class LeaveSpaceState(
|
data class LeaveSpaceState(
|
||||||
val spaceName: String?,
|
val spaceName: String?,
|
||||||
|
val isLastAdmin: Boolean,
|
||||||
val selectableSpaceRooms: AsyncData<ImmutableList<SelectableSpaceRoom>>,
|
val selectableSpaceRooms: AsyncData<ImmutableList<SelectableSpaceRoom>>,
|
||||||
val leaveSpaceAction: AsyncAction<Unit>,
|
val leaveSpaceAction: AsyncAction<Unit>,
|
||||||
val eventSink: (LeaveSpaceEvents) -> 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.
|
* 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.
|
* True if there all the selectable rooms are selected.
|
||||||
|
|
|
||||||
|
|
@ -105,15 +105,20 @@ class LeaveSpaceStateProvider : PreviewParameterProvider<LeaveSpaceState> {
|
||||||
aLeaveSpaceState(
|
aLeaveSpaceState(
|
||||||
selectableSpaceRooms = AsyncData.Failure(Exception("An error")),
|
selectableSpaceRooms = AsyncData.Failure(Exception("An error")),
|
||||||
),
|
),
|
||||||
|
aLeaveSpaceState(
|
||||||
|
isLastAdmin = true,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aLeaveSpaceState(
|
fun aLeaveSpaceState(
|
||||||
spaceName: String? = "Space name",
|
spaceName: String? = "Space name",
|
||||||
|
isLastAdmin: Boolean = false,
|
||||||
selectableSpaceRooms: AsyncData<ImmutableList<SelectableSpaceRoom>> = AsyncData.Uninitialized,
|
selectableSpaceRooms: AsyncData<ImmutableList<SelectableSpaceRoom>> = AsyncData.Uninitialized,
|
||||||
leaveSpaceAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
leaveSpaceAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
) = LeaveSpaceState(
|
) = LeaveSpaceState(
|
||||||
spaceName = spaceName,
|
spaceName = spaceName,
|
||||||
|
isLastAdmin = isLastAdmin,
|
||||||
selectableSpaceRooms = selectableSpaceRooms,
|
selectableSpaceRooms = selectableSpaceRooms,
|
||||||
leaveSpaceAction = leaveSpaceAction,
|
leaveSpaceAction = leaveSpaceAction,
|
||||||
eventSink = { }
|
eventSink = { }
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,10 @@
|
||||||
|
|
||||||
package io.element.android.features.space.impl.leave
|
package io.element.android.features.space.impl.leave
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
|
@ -71,6 +73,12 @@ fun LeaveSpaceView(
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
topBar = {
|
||||||
|
LeaveSpaceHeader(
|
||||||
|
state = state,
|
||||||
|
onBackClick = onCancel,
|
||||||
|
)
|
||||||
|
},
|
||||||
containerColor = ElementTheme.colors.bgCanvasDefault,
|
containerColor = ElementTheme.colors.bgCanvasDefault,
|
||||||
) { padding ->
|
) { padding ->
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -79,45 +87,44 @@ fun LeaveSpaceView(
|
||||||
.imePadding()
|
.imePadding()
|
||||||
.consumeWindowInsets(padding)
|
.consumeWindowInsets(padding)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(16.dp)
|
|
||||||
) {
|
) {
|
||||||
LeaveSpaceHeader(
|
|
||||||
state = state,
|
|
||||||
onBackClick = onCancel,
|
|
||||||
)
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f),
|
.weight(1f),
|
||||||
) {
|
) {
|
||||||
when (state.selectableSpaceRooms) {
|
if (state.isLastAdmin.not()) {
|
||||||
is AsyncData.Success -> {
|
when (state.selectableSpaceRooms) {
|
||||||
// List rooms where the user is the only admin
|
is AsyncData.Success -> {
|
||||||
state.selectableSpaceRooms.data.forEach { selectableSpaceRoom ->
|
// List rooms where the user is the only admin
|
||||||
item {
|
state.selectableSpaceRooms.data.forEach { selectableSpaceRoom ->
|
||||||
SpaceItem(
|
item {
|
||||||
selectableSpaceRoom = selectableSpaceRoom,
|
SpaceItem(
|
||||||
showCheckBox = state.hasOnlyLastAdminRoom.not(),
|
selectableSpaceRoom = selectableSpaceRoom,
|
||||||
onClick = {
|
showCheckBox = state.hasOnlyLastAdminRoom.not(),
|
||||||
state.eventSink(LeaveSpaceEvents.ToggleRoomSelection(selectableSpaceRoom.spaceRoom.roomId))
|
onClick = {
|
||||||
}
|
state.eventSink(LeaveSpaceEvents.ToggleRoomSelection(selectableSpaceRoom.spaceRoom.roomId))
|
||||||
)
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
is AsyncData.Failure -> item {
|
||||||
is AsyncData.Failure -> item {
|
AsyncFailure(
|
||||||
AsyncFailure(
|
throwable = state.selectableSpaceRooms.error,
|
||||||
throwable = state.selectableSpaceRooms.error,
|
onRetry = {
|
||||||
onRetry = null,
|
state.eventSink(LeaveSpaceEvents.Retry)
|
||||||
)
|
},
|
||||||
}
|
)
|
||||||
is AsyncData.Loading,
|
}
|
||||||
AsyncData.Uninitialized -> item {
|
is AsyncData.Loading,
|
||||||
AsyncLoading()
|
AsyncData.Uninitialized -> item {
|
||||||
|
AsyncLoading()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LeaveSpaceButtons(
|
LeaveSpaceButtons(
|
||||||
showLeaveButton = state.selectableSpaceRooms is AsyncData.Success,
|
showLeaveButton = state.showLeaveButton,
|
||||||
selectedRoomsCount = state.selectedRoomsCount,
|
selectedRoomsCount = state.selectedRoomsCount,
|
||||||
onLeaveSpace = {
|
onLeaveSpace = {
|
||||||
state.eventSink(LeaveSpaceEvents.LeaveSpace)
|
state.eventSink(LeaveSpaceEvents.LeaveSpace)
|
||||||
|
|
@ -130,6 +137,7 @@ fun LeaveSpaceView(
|
||||||
AsyncActionView(
|
AsyncActionView(
|
||||||
async = state.leaveSpaceAction,
|
async = state.leaveSpaceAction,
|
||||||
onSuccess = { /* Nothing to do, the screen will be dismissed automatically */ },
|
onSuccess = { /* Nothing to do, the screen will be dismissed automatically */ },
|
||||||
|
errorMessage = { stringResource(CommonStrings.error_unknown) },
|
||||||
onErrorDismiss = { state.eventSink(LeaveSpaceEvents.CloseError) },
|
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),
|
modifier = Modifier.padding(top = 0.dp, bottom = 8.dp, start = 24.dp, end = 24.dp),
|
||||||
iconStyle = BigIcon.Style.AlertSolid,
|
iconStyle = BigIcon.Style.AlertSolid,
|
||||||
title = stringResource(
|
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)
|
state.spaceName ?: stringResource(CommonStrings.common_space)
|
||||||
),
|
),
|
||||||
subTitle =
|
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) {
|
if (state.hasOnlyLastAdminRoom) {
|
||||||
stringResource(R.string.screen_leave_space_subtitle_only_last_admin)
|
stringResource(R.string.screen_leave_space_subtitle_only_last_admin)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -166,34 +176,35 @@ private fun LeaveSpaceHeader(
|
||||||
)
|
)
|
||||||
if (state.showQuickAction) {
|
if (state.showQuickAction) {
|
||||||
if (state.areAllSelected) {
|
if (state.areAllSelected) {
|
||||||
Text(
|
QuickActionButton(CommonStrings.common_deselect_all) {
|
||||||
modifier = Modifier
|
state.eventSink(LeaveSpaceEvents.DeselectAllRooms)
|
||||||
.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,
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
Text(
|
QuickActionButton(resId = CommonStrings.common_select_all) {
|
||||||
modifier = Modifier
|
state.eventSink(LeaveSpaceEvents.SelectAllRooms)
|
||||||
.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,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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
|
@Composable
|
||||||
private fun LeaveSpaceButtons(
|
private fun LeaveSpaceButtons(
|
||||||
showLeaveButton: Boolean,
|
showLeaveButton: Boolean,
|
||||||
|
|
@ -202,7 +213,7 @@ private fun LeaveSpaceButtons(
|
||||||
onCancel: () -> Unit,
|
onCancel: () -> Unit,
|
||||||
) {
|
) {
|
||||||
ButtonColumnMolecule(
|
ButtonColumnMolecule(
|
||||||
modifier = Modifier.padding(top = 16.dp)
|
modifier = Modifier.padding(16.dp)
|
||||||
) {
|
) {
|
||||||
if (showLeaveButton) {
|
if (showLeaveButton) {
|
||||||
val text = if (selectedRoomsCount > 0) {
|
val text = if (selectedRoomsCount > 0) {
|
||||||
|
|
@ -218,6 +229,8 @@ private fun LeaveSpaceButtons(
|
||||||
destructive = true,
|
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(
|
TextButton(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
text = stringResource(CommonStrings.action_cancel),
|
text = stringResource(CommonStrings.action_cancel),
|
||||||
|
|
@ -300,18 +313,15 @@ private fun SpaceItem(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Number of members
|
// Number of members
|
||||||
val subTitle = buildString {
|
val membersCount = pluralStringResource(
|
||||||
append(
|
CommonPlurals.common_member_count,
|
||||||
pluralStringResource(
|
room.numJoinedMembers,
|
||||||
CommonPlurals.common_member_count,
|
room.numJoinedMembers
|
||||||
room.numJoinedMembers,
|
)
|
||||||
room.numJoinedMembers
|
val subTitle = if (selectableSpaceRoom.isLastAdmin) {
|
||||||
)
|
stringResource(R.string.screen_leave_space_last_admin_info, membersCount)
|
||||||
)
|
} else {
|
||||||
if (selectableSpaceRoom.isLastAdmin) {
|
membersCount
|
||||||
append(" ")
|
|
||||||
append(stringResource(R.string.screen_leave_space_last_admin_info))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(end = 16.dp),
|
modifier = Modifier.padding(end = 16.dp),
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,7 @@ private fun LoadingMoreIndicator(
|
||||||
private fun SpaceViewTopBar(
|
private fun SpaceViewTopBar(
|
||||||
currentSpace: SpaceRoom?,
|
currentSpace: SpaceRoom?,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
@Suppress("unused") onLeaveSpaceClick: () -> Unit,
|
onLeaveSpaceClick: () -> Unit,
|
||||||
onShareSpace: () -> Unit,
|
onShareSpace: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|
@ -247,23 +247,25 @@ private fun SpaceViewTopBar(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
/*
|
|
||||||
// TODO re-enable when we have SDK APIs to leave a space
|
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
onClick = {
|
onClick = {
|
||||||
showMenu = false
|
showMenu = false
|
||||||
onLeaveSpaceClick()
|
onLeaveSpaceClick()
|
||||||
},
|
},
|
||||||
text = { Text(stringResource(id = CommonStrings.action_leave)) },
|
text = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = CommonStrings.action_leave),
|
||||||
|
color = ElementTheme.colors.textCriticalPrimary,
|
||||||
|
)
|
||||||
|
},
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = CompoundIcons.Leave(),
|
imageVector = CompoundIcons.Leave(),
|
||||||
tint = ElementTheme.colors.iconSecondary,
|
tint = ElementTheme.colors.iconCriticalPrimary,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<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">
|
<plurals name="screen_leave_space_submit">
|
||||||
<item quantity="one">"Opustit %1$d místnost a prostor"</item>
|
<item quantity="one">"Opustit %1$d místnost a prostor"</item>
|
||||||
<item quantity="few">"Opustit %1$d místnosti 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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<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">
|
<plurals name="screen_leave_space_submit">
|
||||||
<item quantity="one">"Forlad %1$d rum og klynge"</item>
|
<item quantity="one">"Forlad %1$d rum og klynge"</item>
|
||||||
<item quantity="other">"Forlad %1$d rum og klynger"</item>
|
<item quantity="other">"Forlad %1$d rum og klynger"</item>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<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">
|
<plurals name="screen_leave_space_submit">
|
||||||
<item quantity="one">"%1$d Chat und Space verlassen"</item>
|
<item quantity="one">"%1$d Chat und Space verlassen"</item>
|
||||||
<item quantity="other">"%1$d Chats 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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<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">
|
<plurals name="screen_leave_space_submit">
|
||||||
<item quantity="one">"Quitter %1$d salon et l’espace"</item>
|
<item quantity="one">"Quitter %1$d salon et l’espace"</item>
|
||||||
<item quantity="other">"Quitter %1$d salons 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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<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">
|
<plurals name="screen_leave_space_submit">
|
||||||
<item quantity="one">"%1$d szoba és tér elhagyása"</item>
|
<item quantity="one">"%1$d szoba és tér elhagyása"</item>
|
||||||
<item quantity="other">"%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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<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">
|
<plurals name="screen_leave_space_submit">
|
||||||
<item quantity="one">"Leave %1$d room and space"</item>
|
<item quantity="one">"Leave %1$d room and space"</item>
|
||||||
<item quantity="other">"Leave %1$d rooms and space"</item>
|
<item quantity="other">"Leave %1$d rooms and space"</item>
|
||||||
</plurals>
|
</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">"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_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">"Leave %1$s?"</string>
|
||||||
|
<string name="screen_leave_space_title_last_admin">"You are the only admin for %1$s"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -5,60 +5,203 @@
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
|
|
||||||
package io.element.android.features.space.impl.leave
|
package io.element.android.features.space.impl.leave
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
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 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.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.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 io.element.android.tests.testutils.test
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.test.advanceUntilIdle
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class LeaveSpacePresenterTest {
|
class LeaveSpacePresenterTest {
|
||||||
|
private val aSpace = aSpaceRoom(
|
||||||
|
roomId = A_SPACE_ID,
|
||||||
|
name = A_SPACE_NAME,
|
||||||
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state`() = runTest {
|
fun `present - initial state`() = runTest {
|
||||||
val presenter = createLeaveSpacePresenter()
|
val presenter = createLeaveSpacePresenter(
|
||||||
|
leaveSpaceHandle = FakeLeaveSpaceHandle(
|
||||||
|
roomsResult = { Result.success(emptyList()) },
|
||||||
|
),
|
||||||
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val state = awaitItem()
|
val state = awaitItem()
|
||||||
assertThat(state.spaceName).isNull()
|
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)
|
assertThat(state.leaveSpaceAction).isEqualTo(AsyncAction.Uninitialized)
|
||||||
skipItems(1)
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - current space name`() = runTest {
|
fun `present - fail to load rooms`() = runTest {
|
||||||
val fakeSpaceRoomList = FakeSpaceRoomList()
|
|
||||||
val presenter = createLeaveSpacePresenter(
|
val presenter = createLeaveSpacePresenter(
|
||||||
spaceRoomList = fakeSpaceRoomList,
|
leaveSpaceHandle = FakeLeaveSpaceHandle(
|
||||||
|
roomsResult = { Result.failure(AN_EXCEPTION) },
|
||||||
|
)
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val state = awaitItem()
|
val state = awaitItem()
|
||||||
advanceUntilIdle()
|
assertThat(state.selectableSpaceRooms.isLoading()).isTrue()
|
||||||
assertThat(state.spaceName).isNull()
|
assertThat(state.leaveSpaceAction).isEqualTo(AsyncAction.Uninitialized)
|
||||||
val aSpace = aSpaceRoom(
|
skipItems(3)
|
||||||
name = A_SPACE_NAME
|
val stateError = awaitItem()
|
||||||
)
|
assertThat(stateError.selectableSpaceRooms.isFailure()).isTrue()
|
||||||
fakeSpaceRoomList.emitCurrentSpace(aSpace)
|
// Retry
|
||||||
|
stateError.eventSink(LeaveSpaceEvents.Retry)
|
||||||
skipItems(1)
|
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(
|
private fun createLeaveSpacePresenter(
|
||||||
spaceRoomList: SpaceRoomList = FakeSpaceRoomList(),
|
leaveSpaceHandle: LeaveSpaceHandle = FakeLeaveSpaceHandle(),
|
||||||
): LeaveSpacePresenter {
|
): LeaveSpacePresenter {
|
||||||
return 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()
|
selectableSpaceRooms = AsyncData.Loading()
|
||||||
)
|
)
|
||||||
assertThat(sut.showQuickAction).isFalse()
|
assertThat(sut.showQuickAction).isFalse()
|
||||||
|
assertThat(sut.showLeaveButton).isFalse()
|
||||||
assertThat(sut.areAllSelected).isTrue()
|
assertThat(sut.areAllSelected).isTrue()
|
||||||
assertThat(sut.hasOnlyLastAdminRoom).isFalse()
|
assertThat(sut.hasOnlyLastAdminRoom).isFalse()
|
||||||
assertThat(sut.selectedRoomsCount).isEqualTo(0)
|
assertThat(sut.selectedRoomsCount).isEqualTo(0)
|
||||||
|
|
@ -33,11 +34,29 @@ class LeaveSpaceStateTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assertThat(sut.showQuickAction).isFalse()
|
assertThat(sut.showQuickAction).isFalse()
|
||||||
|
assertThat(sut.showLeaveButton).isTrue()
|
||||||
assertThat(sut.areAllSelected).isTrue()
|
assertThat(sut.areAllSelected).isTrue()
|
||||||
assertThat(sut.hasOnlyLastAdminRoom).isFalse()
|
assertThat(sut.hasOnlyLastAdminRoom).isFalse()
|
||||||
assertThat(sut.selectedRoomsCount).isEqualTo(0)
|
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
|
@Test
|
||||||
fun `test no last admin, 1 selected, 1 not selected`() {
|
fun `test no last admin, 1 selected, 1 not selected`() {
|
||||||
val sut = aLeaveSpaceState(
|
val sut = aLeaveSpaceState(
|
||||||
|
|
@ -49,6 +68,7 @@ class LeaveSpaceStateTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assertThat(sut.showQuickAction).isTrue()
|
assertThat(sut.showQuickAction).isTrue()
|
||||||
|
assertThat(sut.showLeaveButton).isTrue()
|
||||||
assertThat(sut.areAllSelected).isFalse()
|
assertThat(sut.areAllSelected).isFalse()
|
||||||
assertThat(sut.hasOnlyLastAdminRoom).isFalse()
|
assertThat(sut.hasOnlyLastAdminRoom).isFalse()
|
||||||
assertThat(sut.selectedRoomsCount).isEqualTo(1)
|
assertThat(sut.selectedRoomsCount).isEqualTo(1)
|
||||||
|
|
@ -65,6 +85,7 @@ class LeaveSpaceStateTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assertThat(sut.showQuickAction).isTrue()
|
assertThat(sut.showQuickAction).isTrue()
|
||||||
|
assertThat(sut.showLeaveButton).isTrue()
|
||||||
assertThat(sut.areAllSelected).isTrue()
|
assertThat(sut.areAllSelected).isTrue()
|
||||||
assertThat(sut.hasOnlyLastAdminRoom).isFalse()
|
assertThat(sut.hasOnlyLastAdminRoom).isFalse()
|
||||||
assertThat(sut.selectedRoomsCount).isEqualTo(2)
|
assertThat(sut.selectedRoomsCount).isEqualTo(2)
|
||||||
|
|
@ -82,6 +103,7 @@ class LeaveSpaceStateTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assertThat(sut.showQuickAction).isTrue()
|
assertThat(sut.showQuickAction).isTrue()
|
||||||
|
assertThat(sut.showLeaveButton).isTrue()
|
||||||
assertThat(sut.areAllSelected).isTrue()
|
assertThat(sut.areAllSelected).isTrue()
|
||||||
assertThat(sut.hasOnlyLastAdminRoom).isFalse()
|
assertThat(sut.hasOnlyLastAdminRoom).isFalse()
|
||||||
assertThat(sut.selectedRoomsCount).isEqualTo(2)
|
assertThat(sut.selectedRoomsCount).isEqualTo(2)
|
||||||
|
|
@ -98,6 +120,7 @@ class LeaveSpaceStateTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assertThat(sut.showQuickAction).isFalse()
|
assertThat(sut.showQuickAction).isFalse()
|
||||||
|
assertThat(sut.showLeaveButton).isTrue()
|
||||||
assertThat(sut.areAllSelected).isTrue()
|
assertThat(sut.areAllSelected).isTrue()
|
||||||
assertThat(sut.hasOnlyLastAdminRoom).isTrue()
|
assertThat(sut.hasOnlyLastAdminRoom).isTrue()
|
||||||
assertThat(sut.selectedRoomsCount).isEqualTo(0)
|
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
|
# 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
|
# 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.
|
# 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
|
# Others
|
||||||
coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }
|
coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }
|
||||||
|
|
|
||||||
|
|
@ -69,14 +69,6 @@ object MatrixPatterns {
|
||||||
str matches PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER
|
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.
|
* Tells if a string is a valid room id.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -7,23 +7,7 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.api.core
|
package io.element.android.libraries.matrix.api.core
|
||||||
|
|
||||||
import io.element.android.libraries.androidutils.metadata.isInDebug
|
typealias SpaceId = RoomId
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Value to use when no space is selected by the user.
|
* 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) {
|
sealed class RecoveryException(message: String) : Exception(message) {
|
||||||
class SecretStorage(message: String) : RecoveryException(message)
|
class SecretStorage(message: String) : RecoveryException(message)
|
||||||
|
class Import(message: String) : RecoveryException(message)
|
||||||
data object BackupExistsOnServer : RecoveryException("BackupExistsOnServer")
|
data object BackupExistsOnServer : RecoveryException("BackupExistsOnServer")
|
||||||
data class Client(val exception: ClientException) : RecoveryException(exception.message ?: "Unknown error")
|
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>>
|
suspend fun joinedSpaces(): Result<List<SpaceRoom>>
|
||||||
|
|
||||||
fun spaceRoomList(id: RoomId): SpaceRoomList
|
fun spaceRoomList(id: RoomId): SpaceRoomList
|
||||||
|
|
||||||
|
fun getLeaveSpaceHandle(spaceId: RoomId): LeaveSpaceHandle
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,8 @@ class RustMatrixClient(
|
||||||
private val innerRoomListService = innerSyncService.roomListService()
|
private val innerRoomListService = innerSyncService.roomListService()
|
||||||
private val innerSpaceService = innerClient.spaceService()
|
private val innerSpaceService = innerClient.spaceService()
|
||||||
|
|
||||||
|
private val roomMembershipObserver = RoomMembershipObserver()
|
||||||
|
|
||||||
private val rustSyncService = RustSyncService(
|
private val rustSyncService = RustSyncService(
|
||||||
inner = innerSyncService,
|
inner = innerSyncService,
|
||||||
dispatcher = sessionDispatcher,
|
dispatcher = sessionDispatcher,
|
||||||
|
|
@ -189,6 +191,7 @@ class RustMatrixClient(
|
||||||
|
|
||||||
override val spaceService: SpaceService = RustSpaceService(
|
override val spaceService: SpaceService = RustSpaceService(
|
||||||
innerSpaceService = innerSpaceService,
|
innerSpaceService = innerSpaceService,
|
||||||
|
roomMembershipObserver = roomMembershipObserver,
|
||||||
sessionCoroutineScope = sessionCoroutineScope,
|
sessionCoroutineScope = sessionCoroutineScope,
|
||||||
sessionDispatcher = sessionDispatcher,
|
sessionDispatcher = sessionDispatcher,
|
||||||
)
|
)
|
||||||
|
|
@ -200,7 +203,7 @@ class RustMatrixClient(
|
||||||
)
|
)
|
||||||
|
|
||||||
private val roomInfoMapper = RoomInfoMapper()
|
private val roomInfoMapper = RoomInfoMapper()
|
||||||
private val roomMembershipObserver = RoomMembershipObserver()
|
|
||||||
private val roomFactory = RustRoomFactory(
|
private val roomFactory = RustRoomFactory(
|
||||||
roomListService = roomListService,
|
roomListService = roomListService,
|
||||||
innerRoomListService = innerRoomListService,
|
innerRoomListService = innerRoomListService,
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,9 @@ fun Throwable.mapRecoveryException(): RecoveryException {
|
||||||
message = errorMessage
|
message = errorMessage
|
||||||
)
|
)
|
||||||
is RustRecoveryException.BackupExistsOnServer -> RecoveryException.BackupExistsOnServer
|
is RustRecoveryException.BackupExistsOnServer -> RecoveryException.BackupExistsOnServer
|
||||||
|
is RustRecoveryException.Import -> RecoveryException.Import(
|
||||||
|
message = errorMessage
|
||||||
|
)
|
||||||
is RustRecoveryException.Client -> RecoveryException.Client(
|
is RustRecoveryException.Client -> RecoveryException.Client(
|
||||||
source.mapClientException()
|
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.coroutine.childScope
|
||||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
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.SpaceRoom
|
||||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
||||||
import io.element.android.libraries.matrix.api.spaces.SpaceService
|
import io.element.android.libraries.matrix.api.spaces.SpaceService
|
||||||
|
|
@ -37,6 +39,7 @@ class RustSpaceService(
|
||||||
private val innerSpaceService: ClientSpaceService,
|
private val innerSpaceService: ClientSpaceService,
|
||||||
private val sessionCoroutineScope: CoroutineScope,
|
private val sessionCoroutineScope: CoroutineScope,
|
||||||
private val sessionDispatcher: CoroutineDispatcher,
|
private val sessionDispatcher: CoroutineDispatcher,
|
||||||
|
private val roomMembershipObserver: RoomMembershipObserver,
|
||||||
) : SpaceService {
|
) : SpaceService {
|
||||||
private val spaceRoomMapper = SpaceRoomMapper()
|
private val spaceRoomMapper = SpaceRoomMapper()
|
||||||
override val spaceRoomsFlow = MutableSharedFlow<List<SpaceRoom>>(replay = 1, extraBufferCapacity = 1)
|
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 {
|
init {
|
||||||
innerSpaceService
|
innerSpaceService
|
||||||
.spaceListUpdate()
|
.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
|
package io.element.android.libraries.matrix.test.spaces
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
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.SpaceRoom
|
||||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
||||||
import io.element.android.libraries.matrix.api.spaces.SpaceService
|
import io.element.android.libraries.matrix.api.spaces.SpaceService
|
||||||
|
|
@ -20,6 +21,7 @@ import kotlinx.coroutines.flow.asSharedFlow
|
||||||
class FakeSpaceService(
|
class FakeSpaceService(
|
||||||
private val joinedSpacesResult: () -> Result<List<SpaceRoom>> = { lambdaError() },
|
private val joinedSpacesResult: () -> Result<List<SpaceRoom>> = { lambdaError() },
|
||||||
private val spaceRoomListResult: (RoomId) -> SpaceRoomList = { lambdaError() },
|
private val spaceRoomListResult: (RoomId) -> SpaceRoomList = { lambdaError() },
|
||||||
|
private val leaveSpaceHandleResult: (RoomId) -> LeaveSpaceHandle = { lambdaError() },
|
||||||
) : SpaceService {
|
) : SpaceService {
|
||||||
private val _spaceRoomsFlow = MutableSharedFlow<List<SpaceRoom>>()
|
private val _spaceRoomsFlow = MutableSharedFlow<List<SpaceRoom>>()
|
||||||
override val spaceRoomsFlow: SharedFlow<List<SpaceRoom>>
|
override val spaceRoomsFlow: SharedFlow<List<SpaceRoom>>
|
||||||
|
|
@ -36,4 +38,8 @@ class FakeSpaceService(
|
||||||
override fun spaceRoomList(id: RoomId): SpaceRoomList {
|
override fun spaceRoomList(id: RoomId): SpaceRoomList {
|
||||||
return spaceRoomListResult(id)
|
return spaceRoomListResult(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getLeaveSpaceHandle(spaceId: RoomId): LeaveSpaceHandle {
|
||||||
|
return leaveSpaceHandleResult(spaceId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:5219016a0af4e1e4f7703dcef3400bf030444845dc0ef52f084e69963170bf1e
|
oid sha256:3c569a42e638a49554127d3dc59c1dda5306e50406a6eaf1033345b44e2fe037
|
||||||
size 13951
|
size 13941
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:964aff2fc49b9d71ebec506bc9acc5010071efca01ed95f240a65b2c91f0f3b1
|
oid sha256:b43c73a9da70133fdff8c63dd6ed8b130b76fbbc28977fae9a34733b0f64faea
|
||||||
size 15852
|
size 15840
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a2097486621a733b1662c02849ec71423d00f9c97c679996fafc8f20a4ef27ce
|
oid sha256:02ea4a02c0901c3d31a86272659f251e3b9299f669177b7a46a9007159989519
|
||||||
size 43844
|
size 44313
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:daff7333af912c8a0d7f009ba10d044aaa7cdfea2dfa3ccff2fd97209bf3278e
|
oid sha256:0a8d851c9393b9a64642d08be96b0c449ce894fdf69d52d0eb664d83c7005520
|
||||||
size 44225
|
size 44211
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:aa05867ac79a3bfb9f59bf1dafb02185274b46e9a5fbf27415a0e907780a0493
|
oid sha256:78cc2ef488b13f72da4d1d149329930816e1d91541bbe62eb48c78b61564dc18
|
||||||
size 36393
|
size 35868
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e74abc389c25fa9a5d33ade6d0f8a2242aeb5150fe81a26e26e9b561c46bb802
|
oid sha256:426847168cf1cf81df60193ba69d1c02cad567f0a72c03f0b2dadb76e92f41c0
|
||||||
size 43010
|
size 42600
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b5c8bdf8d389cccb4cced29aa854a70860e9b44cbdde2a25ab95ccd5bc5e1674
|
oid sha256:479fb84a857b3fcb15a9bba6a6fac3f8e1309a7d5af9c10c1cfe1d16c2eb678f
|
||||||
size 39222
|
size 42208
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:bbf5d7c5320daa0d6b3c6678a6766c7143bc877850257e2c6923b174dfa9b6d4
|
oid sha256:ba7631fe1e5d20946256829e8414f5a305e3a2755d79fc0693fd932bfac40f4a
|
||||||
size 34565
|
size 40182
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:298db58a3f80f4194c5710ceeabec0e49b0e80861b931693d58b1a17f4394b4e
|
oid sha256:6d5be43f0ae09dfea01efa28519b003c6c3bbd8a47c8329ccc9721acdc84a116
|
||||||
size 13873
|
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
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b982dc465fd47dbce6f88321f80c5390ecc90f7e8ad59a76dd44b6c2b9b80c8a
|
oid sha256:1935a53ffe3653f9c094996a7b62359fe70c18ea0b46a7204969f70ce439b5e7
|
||||||
size 13923
|
size 13924
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:00b13582a4a8a380cc502ca2bb181c1ee7c9f2e1685e1d2a75817107d5ebea99
|
oid sha256:f90882b794619797d17c9bdb8ff2e56664a5bd95ab9bee0bb59371f954c0e7ca
|
||||||
size 15393
|
size 15395
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6ad6ae058adb3decb4733c74ad40e4efdd33744eea3e9a793e92fc6b4cbc1464
|
oid sha256:4071cfff533916af1c99636ce6f61e2fbfd8d945dbba1b11e449ce55fd4bd7b9
|
||||||
size 42784
|
size 43148
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ad30950d847bd769e19ce67d8944c16145e929c52210cd54b1ebc5966efe89b5
|
oid sha256:03dd230a42399b5db39a57dbde170955c7f40793fced9420e2fcf8c55820b8c4
|
||||||
size 43221
|
size 42948
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6465f03be41ae02635cbdc94f521059bdd6476c5b2ccfca6f28ccd8898112cea
|
oid sha256:f8d4a0d5f17cf6781d34af68ec0fae1c64cdf88d59ddf4306db411c4c67fcfd8
|
||||||
size 35530
|
size 34945
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:af017745220027708f5a84ca639c9990fe7e474a38aaeecc3cba82011d195360
|
oid sha256:26fcf6cfae843737df553b143d9cbb8049001548b0c3479e1e24a28e0c5e6527
|
||||||
size 42080
|
size 41632
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:73f85bf9ec6c6682e293db2c740d206f6d895d79b3f3d30b967ad55aedbb268c
|
oid sha256:bc8a9e1d6fd7a81cf1ef718d2f3e38babcf96e58e00c2c92827cba0aadd8903e
|
||||||
size 37895
|
size 40613
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:49e2727c42d334c539ee14aab396cadd68b1290e6a256ec2f677f7e4801b30e9
|
oid sha256:1f0ce9d4bbe5a8bdadee5663e8ad8b9b189aa25b5eec7e7c1a40430c09f7d1d3
|
||||||
size 32677
|
size 38108
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6ef494178398bc0f8e7721b7dfde669da4d723aeafa0d4ca6975a58215193416
|
oid sha256:16d9c0ce7ffefb7825efb5e824c06bc721f85f3ff88340abf9c41b2b4679492c
|
||||||
size 13848
|
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