Leave space: use the SDK API.
This commit is contained in:
parent
f726b2a9a4
commit
c36577889d
16 changed files with 493 additions and 118 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -9,65 +9,79 @@ package io.element.android.features.space.impl.leave
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import dev.zacsweers.metro.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 kotlinx.collections.immutable.ImmutableList
|
||||
import io.element.android.libraries.matrix.api.spaces.LeaveSpaceHandle
|
||||
import io.element.android.libraries.matrix.api.spaces.LeaveSpaceRoom
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
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.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
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): LeaveSpaceState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val currentSpace by spaceRoomList.currentSpaceFlow.collectAsState()
|
||||
var currentSpace: LeaveSpaceRoom? by remember { mutableStateOf(null) }
|
||||
val leaveSpaceAction = remember {
|
||||
mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
|
||||
}
|
||||
val selectedRoomIds = remember {
|
||||
mutableStateOf<ImmutableSet<RoomId>>(persistentSetOf())
|
||||
}
|
||||
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
|
||||
val leaveSpaceRooms by produceState(AsyncData.Loading()) {
|
||||
val rooms = leaveSpaceHandle.rooms()
|
||||
val (currentRoom, otherRooms) = rooms.getOrNull()
|
||||
.orEmpty()
|
||||
.partition { it.spaceRoom.roomId == leaveSpaceHandle.id }
|
||||
currentSpace = currentRoom.firstOrNull()
|
||||
// By default select all rooms that can be left
|
||||
selectedRoomIds.value = otherRooms
|
||||
.filter { it.isLastAdmin.not() }
|
||||
.map { it.spaceRoom.roomId }
|
||||
.toPersistentSet()
|
||||
value = rooms.fold(
|
||||
onSuccess = { AsyncData.Success(otherRooms) },
|
||||
onFailure = { AsyncData.Failure(it) }
|
||||
)
|
||||
}
|
||||
val selectableSpaceRooms by produceState<AsyncData<ImmutableList<SelectableSpaceRoom>>>(
|
||||
initialValue = AsyncData.Uninitialized,
|
||||
key1 = joinedSpaceRooms,
|
||||
val selectableSpaceRooms by produceState(
|
||||
initialValue = AsyncData.Loading(),
|
||||
key1 = leaveSpaceRooms,
|
||||
key2 = selectedRoomIds.value,
|
||||
) {
|
||||
value = AsyncData.Success(
|
||||
joinedSpaceRooms.map {
|
||||
value = leaveSpaceRooms.map { list ->
|
||||
list.orEmpty().map { room ->
|
||||
SelectableSpaceRoom(
|
||||
spaceRoom = it,
|
||||
// TODO Get this value from the SDK
|
||||
isLastAdmin = false,
|
||||
isSelected = selectedRoomIds.value.contains(it.roomId),
|
||||
spaceRoom = room.spaceRoom,
|
||||
isLastAdmin = room.isLastAdmin,
|
||||
isSelected = selectedRoomIds.value.contains(room.spaceRoom.roomId),
|
||||
)
|
||||
}.toPersistentList()
|
||||
)
|
||||
}.toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
fun handleEvents(event: LeaveSpaceEvents) {
|
||||
|
|
@ -102,7 +116,8 @@ class LeaveSpacePresenter(
|
|||
}
|
||||
|
||||
return LeaveSpaceState(
|
||||
spaceName = currentSpace.getOrNull()?.name,
|
||||
spaceName = currentSpace?.spaceRoom?.name,
|
||||
isLastAdmin = currentSpace?.isLastAdmin == true,
|
||||
selectableSpaceRooms = selectableSpaceRooms,
|
||||
leaveSpaceAction = leaveSpaceAction.value,
|
||||
eventSink = ::handleEvents,
|
||||
|
|
@ -111,11 +126,10 @@ class LeaveSpacePresenter(
|
|||
|
||||
private fun CoroutineScope.leaveSpace(
|
||||
leaveSpaceAction: MutableState<AsyncAction<Unit>>,
|
||||
@Suppress("unused") selectedRoomIds: Set<RoomId>,
|
||||
selectedRoomIds: Set<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
|
||||
|
|
@ -85,41 +87,42 @@ fun LeaveSpaceView(
|
|||
.imePadding()
|
||||
.consumeWindowInsets(padding)
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
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 = null,
|
||||
)
|
||||
}
|
||||
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)
|
||||
|
|
@ -132,6 +135,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) },
|
||||
)
|
||||
}
|
||||
|
|
@ -152,11 +156,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 {
|
||||
|
|
@ -168,34 +174,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,
|
||||
|
|
@ -204,7 +211,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) {
|
||||
|
|
@ -220,6 +227,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),
|
||||
|
|
@ -302,18 +311,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),
|
||||
|
|
|
|||
|
|
@ -252,11 +252,16 @@ private fun SpaceViewTopBar(
|
|||
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,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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue