Merge pull request #5432 from element-hq/feature/bma/leaveSpace
Leave space: use SDK API.
This commit is contained in:
@@ -10,6 +10,7 @@ package io.element.android.features.space.impl.leave
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
sealed interface LeaveSpaceEvents {
|
||||
data object Retry : LeaveSpaceEvents
|
||||
data object SelectAllRooms : LeaveSpaceEvents
|
||||
data object DeselectAllRooms : LeaveSpaceEvents
|
||||
data class ToggleRoomSelection(val roomId: RoomId) : LeaveSpaceEvents
|
||||
|
||||
@@ -9,21 +9,39 @@ package io.element.android.features.space.impl.leave
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.lifecycle.subscribe
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.space.api.SpaceEntryPoint
|
||||
import io.element.android.features.space.impl.di.SpaceFlowScope
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
|
||||
@ContributesNode(SpaceFlowScope::class)
|
||||
@AssistedInject
|
||||
class LeaveSpaceNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: LeaveSpacePresenter,
|
||||
matrixClient: MatrixClient,
|
||||
presenterFactory: LeaveSpacePresenter.Factory,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
private val inputs: SpaceEntryPoint.Inputs = inputs()
|
||||
private val leaveSpaceHandle = matrixClient.spaceService.getLeaveSpaceHandle(inputs.roomId)
|
||||
private val presenter: LeaveSpacePresenter = presenterFactory.create(leaveSpaceHandle)
|
||||
|
||||
override fun onBuilt() {
|
||||
super.onBuilt()
|
||||
lifecycle.subscribe(
|
||||
onDestroy = {
|
||||
leaveSpaceHandle.close()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
|
||||
@@ -8,92 +8,119 @@
|
||||
package io.element.android.features.space.impl.leave
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import dev.zacsweers.metro.Inject
|
||||
import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.map
|
||||
import io.element.android.libraries.architecture.runUpdatingState
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
||||
import io.element.android.libraries.matrix.api.spaces.LeaveSpaceHandle
|
||||
import io.element.android.libraries.matrix.api.spaces.LeaveSpaceRoom
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.collections.immutable.toPersistentSet
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class LeaveSpacePresenter(
|
||||
private val spaceRoomList: SpaceRoomList,
|
||||
@Assisted private val leaveSpaceHandle: LeaveSpaceHandle,
|
||||
) : Presenter<LeaveSpaceState> {
|
||||
@AssistedFactory
|
||||
fun interface Factory {
|
||||
fun create(leaveSpaceHandle: LeaveSpaceHandle): LeaveSpacePresenter
|
||||
}
|
||||
|
||||
data class LeaveSpaceRooms(
|
||||
val current: LeaveSpaceRoom?,
|
||||
val others: List<LeaveSpaceRoom>,
|
||||
)
|
||||
|
||||
@Composable
|
||||
override fun present(): LeaveSpaceState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val currentSpace by spaceRoomList.currentSpaceFlow.collectAsState()
|
||||
var retryCount by remember { mutableIntStateOf(0) }
|
||||
val leaveSpaceAction = remember {
|
||||
mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
|
||||
}
|
||||
val selectedRoomIds = remember {
|
||||
mutableStateOf<ImmutableSet<RoomId>>(persistentSetOf())
|
||||
var selectedRoomIds by remember {
|
||||
mutableStateOf<Collection<RoomId>>(setOf())
|
||||
}
|
||||
val joinedSpaceRooms by produceState(emptyList()) {
|
||||
// TODO Get the joined room from the SDK, should also have the isLastAdmin boolean
|
||||
val rooms = emptyList<SpaceRoom>()
|
||||
// By default select all rooms
|
||||
selectedRoomIds.value = rooms.map { it.roomId }.toPersistentSet()
|
||||
value = rooms
|
||||
var leaveSpaceRooms by remember {
|
||||
mutableStateOf<AsyncData<LeaveSpaceRooms>>(AsyncData.Loading())
|
||||
}
|
||||
val selectableSpaceRooms by produceState<AsyncData<ImmutableList<SelectableSpaceRoom>>>(
|
||||
initialValue = AsyncData.Uninitialized,
|
||||
key1 = joinedSpaceRooms,
|
||||
key2 = selectedRoomIds.value,
|
||||
) {
|
||||
value = AsyncData.Success(
|
||||
joinedSpaceRooms.map {
|
||||
SelectableSpaceRoom(
|
||||
spaceRoom = it,
|
||||
// TODO Get this value from the SDK
|
||||
isLastAdmin = false,
|
||||
isSelected = selectedRoomIds.value.contains(it.roomId),
|
||||
LaunchedEffect(retryCount) {
|
||||
val rooms = leaveSpaceHandle.rooms()
|
||||
val (currentRoom, otherRooms) = rooms.getOrNull()
|
||||
.orEmpty()
|
||||
.partition { it.spaceRoom.roomId == leaveSpaceHandle.id }
|
||||
// By default select all rooms that can be left
|
||||
selectedRoomIds = otherRooms
|
||||
.filter { it.isLastAdmin.not() }
|
||||
.map { it.spaceRoom.roomId }
|
||||
leaveSpaceRooms = rooms.fold(
|
||||
onSuccess = {
|
||||
AsyncData.Success(
|
||||
LeaveSpaceRooms(
|
||||
current = currentRoom.firstOrNull(),
|
||||
others = otherRooms.toImmutableList(),
|
||||
)
|
||||
)
|
||||
}.toPersistentList()
|
||||
},
|
||||
onFailure = { AsyncData.Failure(it) }
|
||||
)
|
||||
}
|
||||
var selectableSpaceRooms by remember {
|
||||
mutableStateOf<AsyncData<ImmutableList<SelectableSpaceRoom>>>(AsyncData.Loading())
|
||||
}
|
||||
LaunchedEffect(selectedRoomIds, leaveSpaceRooms) {
|
||||
selectableSpaceRooms = leaveSpaceRooms.map {
|
||||
it?.others.orEmpty().map { room ->
|
||||
SelectableSpaceRoom(
|
||||
spaceRoom = room.spaceRoom,
|
||||
isLastAdmin = room.isLastAdmin,
|
||||
isSelected = selectedRoomIds.contains(room.spaceRoom.roomId),
|
||||
)
|
||||
}.toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
fun handleEvents(event: LeaveSpaceEvents) {
|
||||
when (event) {
|
||||
LeaveSpaceEvents.Retry -> {
|
||||
leaveSpaceRooms = AsyncData.Loading()
|
||||
retryCount += 1
|
||||
}
|
||||
LeaveSpaceEvents.DeselectAllRooms -> {
|
||||
selectedRoomIds.value = persistentSetOf()
|
||||
selectedRoomIds = persistentSetOf()
|
||||
}
|
||||
LeaveSpaceEvents.SelectAllRooms -> {
|
||||
selectedRoomIds.value = selectableSpaceRooms.dataOrNull()
|
||||
selectedRoomIds = selectableSpaceRooms.dataOrNull()
|
||||
.orEmpty()
|
||||
.filter { it.isLastAdmin.not() }
|
||||
.map { it.spaceRoom.roomId }
|
||||
.toPersistentSet()
|
||||
}
|
||||
is LeaveSpaceEvents.ToggleRoomSelection -> {
|
||||
val currentSet = selectedRoomIds.value
|
||||
selectedRoomIds.value = if (currentSet.contains(event.roomId)) {
|
||||
currentSet - event.roomId
|
||||
selectedRoomIds = if (selectedRoomIds.contains(event.roomId)) {
|
||||
selectedRoomIds - event.roomId
|
||||
} else {
|
||||
currentSet + event.roomId
|
||||
selectedRoomIds + event.roomId
|
||||
}
|
||||
.toPersistentSet()
|
||||
}
|
||||
LeaveSpaceEvents.LeaveSpace -> coroutineScope.leaveSpace(
|
||||
leaveSpaceAction = leaveSpaceAction,
|
||||
selectedRoomIds = selectedRoomIds.value,
|
||||
selectedRoomIds = selectedRoomIds,
|
||||
)
|
||||
LeaveSpaceEvents.CloseError -> {
|
||||
leaveSpaceAction.value = AsyncAction.Uninitialized
|
||||
@@ -102,7 +129,8 @@ class LeaveSpacePresenter(
|
||||
}
|
||||
|
||||
return LeaveSpaceState(
|
||||
spaceName = currentSpace.getOrNull()?.name,
|
||||
spaceName = leaveSpaceRooms.dataOrNull()?.current?.spaceRoom?.name,
|
||||
isLastAdmin = leaveSpaceRooms.dataOrNull()?.current?.isLastAdmin == true,
|
||||
selectableSpaceRooms = selectableSpaceRooms,
|
||||
leaveSpaceAction = leaveSpaceAction.value,
|
||||
eventSink = ::handleEvents,
|
||||
@@ -111,11 +139,10 @@ class LeaveSpacePresenter(
|
||||
|
||||
private fun CoroutineScope.leaveSpace(
|
||||
leaveSpaceAction: MutableState<AsyncAction<Unit>>,
|
||||
@Suppress("unused") selectedRoomIds: Set<RoomId>,
|
||||
selectedRoomIds: Collection<RoomId>,
|
||||
) = launch {
|
||||
runUpdatingState(leaveSpaceAction) {
|
||||
// TODO SDK API call to leave all the rooms and space
|
||||
Result.failure(Exception("Not implemented"))
|
||||
leaveSpaceHandle.leave(selectedRoomIds.toList())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class LeaveSpaceState(
|
||||
val spaceName: String?,
|
||||
val isLastAdmin: Boolean,
|
||||
val selectableSpaceRooms: AsyncData<ImmutableList<SelectableSpaceRoom>>,
|
||||
val leaveSpaceAction: AsyncAction<Unit>,
|
||||
val eventSink: (LeaveSpaceEvents) -> Unit,
|
||||
@@ -25,7 +26,12 @@ data class LeaveSpaceState(
|
||||
/**
|
||||
* True if we should show the quick action to select/deselect all rooms.
|
||||
*/
|
||||
val showQuickAction = selectableRooms.isNotEmpty()
|
||||
val showQuickAction = isLastAdmin.not() && selectableRooms.isNotEmpty()
|
||||
|
||||
/**
|
||||
* True if we should show the leave button.
|
||||
*/
|
||||
val showLeaveButton = isLastAdmin.not() && selectableSpaceRooms is AsyncData.Success
|
||||
|
||||
/**
|
||||
* True if there all the selectable rooms are selected.
|
||||
|
||||
@@ -105,15 +105,20 @@ class LeaveSpaceStateProvider : PreviewParameterProvider<LeaveSpaceState> {
|
||||
aLeaveSpaceState(
|
||||
selectableSpaceRooms = AsyncData.Failure(Exception("An error")),
|
||||
),
|
||||
aLeaveSpaceState(
|
||||
isLastAdmin = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun aLeaveSpaceState(
|
||||
spaceName: String? = "Space name",
|
||||
isLastAdmin: Boolean = false,
|
||||
selectableSpaceRooms: AsyncData<ImmutableList<SelectableSpaceRoom>> = AsyncData.Uninitialized,
|
||||
leaveSpaceAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
) = LeaveSpaceState(
|
||||
spaceName = spaceName,
|
||||
isLastAdmin = isLastAdmin,
|
||||
selectableSpaceRooms = selectableSpaceRooms,
|
||||
leaveSpaceAction = leaveSpaceAction,
|
||||
eventSink = { }
|
||||
|
||||
@@ -9,8 +9,10 @@
|
||||
|
||||
package io.element.android.features.space.impl.leave
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -71,6 +73,12 @@ fun LeaveSpaceView(
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
LeaveSpaceHeader(
|
||||
state = state,
|
||||
onBackClick = onCancel,
|
||||
)
|
||||
},
|
||||
containerColor = ElementTheme.colors.bgCanvasDefault,
|
||||
) { padding ->
|
||||
Column(
|
||||
@@ -79,45 +87,44 @@ fun LeaveSpaceView(
|
||||
.imePadding()
|
||||
.consumeWindowInsets(padding)
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
LeaveSpaceHeader(
|
||||
state = state,
|
||||
onBackClick = onCancel,
|
||||
)
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
) {
|
||||
when (state.selectableSpaceRooms) {
|
||||
is AsyncData.Success -> {
|
||||
// List rooms where the user is the only admin
|
||||
state.selectableSpaceRooms.data.forEach { selectableSpaceRoom ->
|
||||
item {
|
||||
SpaceItem(
|
||||
selectableSpaceRoom = selectableSpaceRoom,
|
||||
showCheckBox = state.hasOnlyLastAdminRoom.not(),
|
||||
onClick = {
|
||||
state.eventSink(LeaveSpaceEvents.ToggleRoomSelection(selectableSpaceRoom.spaceRoom.roomId))
|
||||
}
|
||||
)
|
||||
if (state.isLastAdmin.not()) {
|
||||
when (state.selectableSpaceRooms) {
|
||||
is AsyncData.Success -> {
|
||||
// List rooms where the user is the only admin
|
||||
state.selectableSpaceRooms.data.forEach { selectableSpaceRoom ->
|
||||
item {
|
||||
SpaceItem(
|
||||
selectableSpaceRoom = selectableSpaceRoom,
|
||||
showCheckBox = state.hasOnlyLastAdminRoom.not(),
|
||||
onClick = {
|
||||
state.eventSink(LeaveSpaceEvents.ToggleRoomSelection(selectableSpaceRoom.spaceRoom.roomId))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is AsyncData.Failure -> item {
|
||||
AsyncFailure(
|
||||
throwable = state.selectableSpaceRooms.error,
|
||||
onRetry = null,
|
||||
)
|
||||
}
|
||||
is AsyncData.Loading,
|
||||
AsyncData.Uninitialized -> item {
|
||||
AsyncLoading()
|
||||
is AsyncData.Failure -> item {
|
||||
AsyncFailure(
|
||||
throwable = state.selectableSpaceRooms.error,
|
||||
onRetry = {
|
||||
state.eventSink(LeaveSpaceEvents.Retry)
|
||||
},
|
||||
)
|
||||
}
|
||||
is AsyncData.Loading,
|
||||
AsyncData.Uninitialized -> item {
|
||||
AsyncLoading()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
LeaveSpaceButtons(
|
||||
showLeaveButton = state.selectableSpaceRooms is AsyncData.Success,
|
||||
showLeaveButton = state.showLeaveButton,
|
||||
selectedRoomsCount = state.selectedRoomsCount,
|
||||
onLeaveSpace = {
|
||||
state.eventSink(LeaveSpaceEvents.LeaveSpace)
|
||||
@@ -130,6 +137,7 @@ fun LeaveSpaceView(
|
||||
AsyncActionView(
|
||||
async = state.leaveSpaceAction,
|
||||
onSuccess = { /* Nothing to do, the screen will be dismissed automatically */ },
|
||||
errorMessage = { stringResource(CommonStrings.error_unknown) },
|
||||
onErrorDismiss = { state.eventSink(LeaveSpaceEvents.CloseError) },
|
||||
)
|
||||
}
|
||||
@@ -150,11 +158,13 @@ private fun LeaveSpaceHeader(
|
||||
modifier = Modifier.padding(top = 0.dp, bottom = 8.dp, start = 24.dp, end = 24.dp),
|
||||
iconStyle = BigIcon.Style.AlertSolid,
|
||||
title = stringResource(
|
||||
R.string.screen_leave_space_title,
|
||||
if (state.isLastAdmin) R.string.screen_leave_space_title_last_admin else R.string.screen_leave_space_title,
|
||||
state.spaceName ?: stringResource(CommonStrings.common_space)
|
||||
),
|
||||
subTitle =
|
||||
if (state.selectableSpaceRooms is AsyncData.Success && state.selectableSpaceRooms.data.isNotEmpty()) {
|
||||
if (state.isLastAdmin) {
|
||||
stringResource(R.string.screen_leave_space_subtitle_last_admin)
|
||||
} else if (state.selectableSpaceRooms is AsyncData.Success && state.selectableSpaceRooms.data.isNotEmpty()) {
|
||||
if (state.hasOnlyLastAdminRoom) {
|
||||
stringResource(R.string.screen_leave_space_subtitle_only_last_admin)
|
||||
} else {
|
||||
@@ -166,34 +176,35 @@ private fun LeaveSpaceHeader(
|
||||
)
|
||||
if (state.showQuickAction) {
|
||||
if (state.areAllSelected) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.align(Alignment.End)
|
||||
.clickable {
|
||||
state.eventSink(LeaveSpaceEvents.DeselectAllRooms)
|
||||
}
|
||||
.padding(vertical = 8.dp, horizontal = 8.dp),
|
||||
text = stringResource(CommonStrings.common_deselect_all),
|
||||
color = ElementTheme.colors.textActionPrimary,
|
||||
style = ElementTheme.typography.fontBodyMdMedium,
|
||||
)
|
||||
QuickActionButton(CommonStrings.common_deselect_all) {
|
||||
state.eventSink(LeaveSpaceEvents.DeselectAllRooms)
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.align(Alignment.End)
|
||||
.clickable {
|
||||
state.eventSink(LeaveSpaceEvents.SelectAllRooms)
|
||||
}
|
||||
.padding(vertical = 8.dp, horizontal = 8.dp),
|
||||
text = stringResource(CommonStrings.common_select_all),
|
||||
color = ElementTheme.colors.textActionPrimary,
|
||||
style = ElementTheme.typography.fontBodyMdMedium,
|
||||
)
|
||||
QuickActionButton(resId = CommonStrings.common_select_all) {
|
||||
state.eventSink(LeaveSpaceEvents.SelectAllRooms)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.QuickActionButton(
|
||||
@StringRes resId: Int,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.align(Alignment.End)
|
||||
.padding(end = 8.dp)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(8.dp),
|
||||
text = stringResource(resId),
|
||||
color = ElementTheme.colors.textActionPrimary,
|
||||
style = ElementTheme.typography.fontBodyMdMedium,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LeaveSpaceButtons(
|
||||
showLeaveButton: Boolean,
|
||||
@@ -202,7 +213,7 @@ private fun LeaveSpaceButtons(
|
||||
onCancel: () -> Unit,
|
||||
) {
|
||||
ButtonColumnMolecule(
|
||||
modifier = Modifier.padding(top = 16.dp)
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
if (showLeaveButton) {
|
||||
val text = if (selectedRoomsCount > 0) {
|
||||
@@ -218,6 +229,8 @@ private fun LeaveSpaceButtons(
|
||||
destructive = true,
|
||||
)
|
||||
}
|
||||
// TODO For least admin space, add a button to open the settings.
|
||||
// See https://www.figma.com/design/kcnHxunG1LDWXsJhaNuiHz/ER-145--Spaces-on-Element-X?node-id=4622-59600
|
||||
TextButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResource(CommonStrings.action_cancel),
|
||||
@@ -300,18 +313,15 @@ private fun SpaceItem(
|
||||
)
|
||||
}
|
||||
// Number of members
|
||||
val subTitle = buildString {
|
||||
append(
|
||||
pluralStringResource(
|
||||
CommonPlurals.common_member_count,
|
||||
room.numJoinedMembers,
|
||||
room.numJoinedMembers
|
||||
)
|
||||
)
|
||||
if (selectableSpaceRoom.isLastAdmin) {
|
||||
append(" ")
|
||||
append(stringResource(R.string.screen_leave_space_last_admin_info))
|
||||
}
|
||||
val membersCount = pluralStringResource(
|
||||
CommonPlurals.common_member_count,
|
||||
room.numJoinedMembers,
|
||||
room.numJoinedMembers
|
||||
)
|
||||
val subTitle = if (selectableSpaceRoom.isLastAdmin) {
|
||||
stringResource(R.string.screen_leave_space_last_admin_info, membersCount)
|
||||
} else {
|
||||
membersCount
|
||||
}
|
||||
Text(
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
|
||||
@@ -202,7 +202,7 @@ private fun LoadingMoreIndicator(
|
||||
private fun SpaceViewTopBar(
|
||||
currentSpace: SpaceRoom?,
|
||||
onBackClick: () -> Unit,
|
||||
@Suppress("unused") onLeaveSpaceClick: () -> Unit,
|
||||
onLeaveSpaceClick: () -> Unit,
|
||||
onShareSpace: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -247,23 +247,25 @@ private fun SpaceViewTopBar(
|
||||
)
|
||||
}
|
||||
)
|
||||
/*
|
||||
// TODO re-enable when we have SDK APIs to leave a space
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
showMenu = false
|
||||
onLeaveSpaceClick()
|
||||
},
|
||||
text = { Text(stringResource(id = CommonStrings.action_leave)) },
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(id = CommonStrings.action_leave),
|
||||
color = ElementTheme.colors.textCriticalPrimary,
|
||||
)
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Leave(),
|
||||
tint = ElementTheme.colors.iconSecondary,
|
||||
tint = ElementTheme.colors.iconCriticalPrimary,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
)
|
||||
*/
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_leave_space_last_admin_info">"(Správce)"</string>
|
||||
<plurals name="screen_leave_space_submit">
|
||||
<item quantity="one">"Opustit %1$d místnost a prostor"</item>
|
||||
<item quantity="few">"Opustit %1$d místnosti a prostor"</item>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_leave_space_last_admin_info">"Administrator"</string>
|
||||
<plurals name="screen_leave_space_submit">
|
||||
<item quantity="one">"Forlad %1$d rum og klynge"</item>
|
||||
<item quantity="other">"Forlad %1$d rum og klynger"</item>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_leave_space_last_admin_info">"(Admin)"</string>
|
||||
<plurals name="screen_leave_space_submit">
|
||||
<item quantity="one">"%1$d Chat und Space verlassen"</item>
|
||||
<item quantity="other">"%1$d Chats und Space verlassen"</item>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_leave_space_last_admin_info">"(Admin)"</string>
|
||||
<plurals name="screen_leave_space_submit">
|
||||
<item quantity="one">"Quitter %1$d salon et l’espace"</item>
|
||||
<item quantity="other">"Quitter %1$d salons et l’espace"</item>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_leave_space_last_admin_info">"(Adminisztrátor)"</string>
|
||||
<plurals name="screen_leave_space_submit">
|
||||
<item quantity="one">"%1$d szoba és tér elhagyása"</item>
|
||||
<item quantity="other">"%1$d szoba és tér elhagyása"</item>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_leave_space_last_admin_info">"(Admin)"</string>
|
||||
<string name="screen_leave_space_last_admin_info">"%1$s (Admin)"</string>
|
||||
<plurals name="screen_leave_space_submit">
|
||||
<item quantity="one">"Leave %1$d room and space"</item>
|
||||
<item quantity="other">"Leave %1$d rooms and space"</item>
|
||||
</plurals>
|
||||
<string name="screen_leave_space_subtitle">"Select the rooms you’d like to leave which you\'re not the only administrator for:"</string>
|
||||
<string name="screen_leave_space_subtitle_last_admin">"You need to assign another admin for this space before you can leave."</string>
|
||||
<string name="screen_leave_space_subtitle_only_last_admin">"You will not be removed from the following room(s) because you\'re the only administrator:"</string>
|
||||
<string name="screen_leave_space_title">"Leave %1$s?"</string>
|
||||
<string name="screen_leave_space_title_last_admin">"You are the only admin for %1$s"</string>
|
||||
</resources>
|
||||
|
||||
@@ -5,60 +5,203 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.features.space.impl.leave
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.spaces.LeaveSpaceHandle
|
||||
import io.element.android.libraries.matrix.api.spaces.LeaveSpaceRoom
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_SPACE_ID
|
||||
import io.element.android.libraries.matrix.test.A_SPACE_NAME
|
||||
import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList
|
||||
import io.element.android.libraries.matrix.test.spaces.FakeLeaveSpaceHandle
|
||||
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import io.element.android.tests.testutils.test
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class LeaveSpacePresenterTest {
|
||||
private val aSpace = aSpaceRoom(
|
||||
roomId = A_SPACE_ID,
|
||||
name = A_SPACE_NAME,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = createLeaveSpacePresenter()
|
||||
val presenter = createLeaveSpacePresenter(
|
||||
leaveSpaceHandle = FakeLeaveSpaceHandle(
|
||||
roomsResult = { Result.success(emptyList()) },
|
||||
),
|
||||
)
|
||||
presenter.test {
|
||||
val state = awaitItem()
|
||||
assertThat(state.spaceName).isNull()
|
||||
assertThat(state.selectableSpaceRooms).isEqualTo(AsyncData.Uninitialized)
|
||||
assertThat(state.isLastAdmin).isFalse()
|
||||
assertThat(state.selectableSpaceRooms.isLoading()).isTrue()
|
||||
assertThat(state.leaveSpaceAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
skipItems(1)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - current space name`() = runTest {
|
||||
val fakeSpaceRoomList = FakeSpaceRoomList()
|
||||
fun `present - fail to load rooms`() = runTest {
|
||||
val presenter = createLeaveSpacePresenter(
|
||||
spaceRoomList = fakeSpaceRoomList,
|
||||
leaveSpaceHandle = FakeLeaveSpaceHandle(
|
||||
roomsResult = { Result.failure(AN_EXCEPTION) },
|
||||
)
|
||||
)
|
||||
presenter.test {
|
||||
val state = awaitItem()
|
||||
advanceUntilIdle()
|
||||
assertThat(state.spaceName).isNull()
|
||||
val aSpace = aSpaceRoom(
|
||||
name = A_SPACE_NAME
|
||||
)
|
||||
fakeSpaceRoomList.emitCurrentSpace(aSpace)
|
||||
assertThat(state.selectableSpaceRooms.isLoading()).isTrue()
|
||||
assertThat(state.leaveSpaceAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
skipItems(3)
|
||||
val stateError = awaitItem()
|
||||
assertThat(stateError.selectableSpaceRooms.isFailure()).isTrue()
|
||||
// Retry
|
||||
stateError.eventSink(LeaveSpaceEvents.Retry)
|
||||
skipItems(1)
|
||||
assertThat(awaitItem().spaceName).isEqualTo(A_SPACE_NAME)
|
||||
val stateLoadingAgain = awaitItem()
|
||||
assertThat(stateLoadingAgain.selectableSpaceRooms.isLoading()).isTrue()
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - current space name and is last admin`() = runTest {
|
||||
val presenter = createLeaveSpacePresenter(
|
||||
leaveSpaceHandle = FakeLeaveSpaceHandle(
|
||||
roomsResult = { Result.success(listOf(aLeaveSpaceRoom(spaceRoom = aSpace, isLastAdmin = true))) },
|
||||
)
|
||||
)
|
||||
presenter.test {
|
||||
val state = awaitItem()
|
||||
assertThat(state.spaceName).isNull()
|
||||
skipItems(3)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.spaceName).isEqualTo(A_SPACE_NAME)
|
||||
assertThat(finalState.isLastAdmin).isTrue()
|
||||
// The current state is not in the sub room list
|
||||
assertThat(finalState.selectableSpaceRooms.dataOrNull()!!).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - leave space and sub rooms`() = runTest {
|
||||
val leaveResult = lambdaRecorder<List<RoomId>, Result<Unit>> { Result.success(Unit) }
|
||||
val presenter = createLeaveSpacePresenter(
|
||||
leaveSpaceHandle = FakeLeaveSpaceHandle(
|
||||
roomsResult = {
|
||||
Result.success(
|
||||
listOf(
|
||||
LeaveSpaceRoom(aSpaceRoom(roomId = A_ROOM_ID), isLastAdmin = false),
|
||||
LeaveSpaceRoom(aSpaceRoom(roomId = A_ROOM_ID_2), isLastAdmin = true),
|
||||
)
|
||||
)
|
||||
},
|
||||
leaveResult = leaveResult,
|
||||
)
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(4)
|
||||
val state = awaitItem()
|
||||
assertThat(state.spaceName).isNull()
|
||||
assertThat(state.isLastAdmin).isFalse()
|
||||
val data = state.selectableSpaceRooms.dataOrNull()!!
|
||||
assertThat(data.size).isEqualTo(2)
|
||||
// Only one room is selectable as the user is the last admin in the other one
|
||||
val room1 = data[0]
|
||||
assertThat(room1.spaceRoom.roomId).isEqualTo(A_ROOM_ID)
|
||||
assertThat(room1.isSelected).isTrue()
|
||||
assertThat(room1.isLastAdmin).isFalse()
|
||||
val room2 = data[1]
|
||||
assertThat(room2.spaceRoom.roomId).isEqualTo(A_ROOM_ID_2)
|
||||
assertThat(room2.isSelected).isFalse()
|
||||
assertThat(room2.isLastAdmin).isTrue()
|
||||
// Deselect all
|
||||
state.eventSink(LeaveSpaceEvents.DeselectAllRooms)
|
||||
skipItems(1)
|
||||
val stateAllDeselected = awaitItem()
|
||||
val dataAllDeselected = stateAllDeselected.selectableSpaceRooms.dataOrNull()!!
|
||||
assertThat(dataAllDeselected.any { it.isSelected }).isFalse()
|
||||
// Select all
|
||||
stateAllDeselected.eventSink(LeaveSpaceEvents.SelectAllRooms)
|
||||
skipItems(1)
|
||||
val stateAllSelected = awaitItem()
|
||||
val dataAllSelected = stateAllSelected.selectableSpaceRooms.dataOrNull()!!
|
||||
// The last admin room should not be selected
|
||||
assertThat(dataAllSelected.count { it.isSelected }).isEqualTo(1)
|
||||
// Toggle selection of the first room
|
||||
stateAllSelected.eventSink(LeaveSpaceEvents.ToggleRoomSelection(A_ROOM_ID))
|
||||
skipItems(1)
|
||||
val stateOneDeselected = awaitItem()
|
||||
val dataOneDeselected = stateOneDeselected.selectableSpaceRooms.dataOrNull()!!
|
||||
assertThat(dataOneDeselected[0].isSelected).isFalse()
|
||||
// Toggle selection of the first room
|
||||
stateOneDeselected.eventSink(LeaveSpaceEvents.ToggleRoomSelection(A_ROOM_ID))
|
||||
skipItems(1)
|
||||
val stateOneSelected = awaitItem()
|
||||
val dataOneSelected = stateOneSelected.selectableSpaceRooms.dataOrNull()!!
|
||||
assertThat(dataOneSelected[0].isSelected).isTrue()
|
||||
// Leave space
|
||||
stateOneSelected.eventSink(LeaveSpaceEvents.LeaveSpace)
|
||||
val stateLeaving = awaitItem()
|
||||
assertThat(stateLeaving.leaveSpaceAction).isEqualTo(AsyncAction.Loading)
|
||||
val stateLeft = awaitItem()
|
||||
assertThat(stateLeft.leaveSpaceAction.isSuccess()).isTrue()
|
||||
leaveResult.assertions().isCalledOnce().with(
|
||||
value(listOf(A_ROOM_ID))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - leave space error and close`() = runTest {
|
||||
val leaveResult = lambdaRecorder<List<RoomId>, Result<Unit>> {
|
||||
Result.failure(AN_EXCEPTION)
|
||||
}
|
||||
val presenter = createLeaveSpacePresenter(
|
||||
leaveSpaceHandle = FakeLeaveSpaceHandle(
|
||||
roomsResult = { Result.success(emptyList()) },
|
||||
leaveResult = leaveResult,
|
||||
)
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(4)
|
||||
val state = awaitItem()
|
||||
state.eventSink(LeaveSpaceEvents.LeaveSpace)
|
||||
val stateLeaving = awaitItem()
|
||||
assertThat(stateLeaving.leaveSpaceAction).isEqualTo(AsyncAction.Loading)
|
||||
val stateError = awaitItem()
|
||||
assertThat(stateError.leaveSpaceAction.isFailure()).isTrue()
|
||||
// Close error
|
||||
stateError.eventSink(LeaveSpaceEvents.CloseError)
|
||||
val stateErrorClosed = awaitItem()
|
||||
assertThat(stateErrorClosed.leaveSpaceAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createLeaveSpacePresenter(
|
||||
spaceRoomList: SpaceRoomList = FakeSpaceRoomList(),
|
||||
leaveSpaceHandle: LeaveSpaceHandle = FakeLeaveSpaceHandle(),
|
||||
): LeaveSpacePresenter {
|
||||
return LeaveSpacePresenter(
|
||||
spaceRoomList = spaceRoomList,
|
||||
leaveSpaceHandle = leaveSpaceHandle,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun aLeaveSpaceRoom(
|
||||
spaceRoom: SpaceRoom = aSpaceRoom(
|
||||
roomId = A_SPACE_ID,
|
||||
name = A_SPACE_NAME,
|
||||
),
|
||||
isLastAdmin: Boolean = false,
|
||||
) = LeaveSpaceRoom(
|
||||
spaceRoom = spaceRoom,
|
||||
isLastAdmin = isLastAdmin,
|
||||
)
|
||||
|
||||
@@ -20,6 +20,7 @@ class LeaveSpaceStateTest {
|
||||
selectableSpaceRooms = AsyncData.Loading()
|
||||
)
|
||||
assertThat(sut.showQuickAction).isFalse()
|
||||
assertThat(sut.showLeaveButton).isFalse()
|
||||
assertThat(sut.areAllSelected).isTrue()
|
||||
assertThat(sut.hasOnlyLastAdminRoom).isFalse()
|
||||
assertThat(sut.selectedRoomsCount).isEqualTo(0)
|
||||
@@ -33,11 +34,29 @@ class LeaveSpaceStateTest {
|
||||
)
|
||||
)
|
||||
assertThat(sut.showQuickAction).isFalse()
|
||||
assertThat(sut.showLeaveButton).isTrue()
|
||||
assertThat(sut.areAllSelected).isTrue()
|
||||
assertThat(sut.hasOnlyLastAdminRoom).isFalse()
|
||||
assertThat(sut.selectedRoomsCount).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test last admin`() {
|
||||
val sut = aLeaveSpaceState(
|
||||
isLastAdmin = true,
|
||||
selectableSpaceRooms = AsyncData.Success(
|
||||
persistentListOf(
|
||||
aSelectableSpaceRoom(isLastAdmin = false, isSelected = false),
|
||||
)
|
||||
)
|
||||
)
|
||||
assertThat(sut.showQuickAction).isFalse()
|
||||
assertThat(sut.showLeaveButton).isFalse()
|
||||
assertThat(sut.areAllSelected).isFalse()
|
||||
assertThat(sut.hasOnlyLastAdminRoom).isFalse()
|
||||
assertThat(sut.selectedRoomsCount).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test no last admin, 1 selected, 1 not selected`() {
|
||||
val sut = aLeaveSpaceState(
|
||||
@@ -49,6 +68,7 @@ class LeaveSpaceStateTest {
|
||||
)
|
||||
)
|
||||
assertThat(sut.showQuickAction).isTrue()
|
||||
assertThat(sut.showLeaveButton).isTrue()
|
||||
assertThat(sut.areAllSelected).isFalse()
|
||||
assertThat(sut.hasOnlyLastAdminRoom).isFalse()
|
||||
assertThat(sut.selectedRoomsCount).isEqualTo(1)
|
||||
@@ -65,6 +85,7 @@ class LeaveSpaceStateTest {
|
||||
)
|
||||
)
|
||||
assertThat(sut.showQuickAction).isTrue()
|
||||
assertThat(sut.showLeaveButton).isTrue()
|
||||
assertThat(sut.areAllSelected).isTrue()
|
||||
assertThat(sut.hasOnlyLastAdminRoom).isFalse()
|
||||
assertThat(sut.selectedRoomsCount).isEqualTo(2)
|
||||
@@ -82,6 +103,7 @@ class LeaveSpaceStateTest {
|
||||
)
|
||||
)
|
||||
assertThat(sut.showQuickAction).isTrue()
|
||||
assertThat(sut.showLeaveButton).isTrue()
|
||||
assertThat(sut.areAllSelected).isTrue()
|
||||
assertThat(sut.hasOnlyLastAdminRoom).isFalse()
|
||||
assertThat(sut.selectedRoomsCount).isEqualTo(2)
|
||||
@@ -98,6 +120,7 @@ class LeaveSpaceStateTest {
|
||||
)
|
||||
)
|
||||
assertThat(sut.showQuickAction).isFalse()
|
||||
assertThat(sut.showLeaveButton).isTrue()
|
||||
assertThat(sut.areAllSelected).isTrue()
|
||||
assertThat(sut.hasOnlyLastAdminRoom).isTrue()
|
||||
assertThat(sut.selectedRoomsCount).isEqualTo(0)
|
||||
|
||||
Reference in New Issue
Block a user