Add manage mode to space view for removing child rooms, wip.
This commit is contained in:
parent
168ea3e0e4
commit
78b4895254
11 changed files with 263 additions and 21 deletions
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
package io.element.android.features.space.impl.root
|
package io.element.android.features.space.impl.root
|
||||||
|
|
||||||
|
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.SpaceRoom
|
||||||
|
|
||||||
sealed interface SpaceEvents {
|
sealed interface SpaceEvents {
|
||||||
|
|
@ -19,4 +20,12 @@ sealed interface SpaceEvents {
|
||||||
|
|
||||||
data class ShowTopicViewer(val topic: String) : SpaceEvents
|
data class ShowTopicViewer(val topic: String) : SpaceEvents
|
||||||
data object HideTopicViewer : SpaceEvents
|
data object HideTopicViewer : SpaceEvents
|
||||||
|
|
||||||
|
// Manage mode events
|
||||||
|
data object EnterManageMode : SpaceEvents
|
||||||
|
data object ExitManageMode : SpaceEvents
|
||||||
|
data class ToggleRoomSelection(val roomId: RoomId) : SpaceEvents
|
||||||
|
data object ConfirmRoomRemoval : SpaceEvents
|
||||||
|
data object RemoveSelectedRooms : SpaceEvents
|
||||||
|
data object ClearRemoveAction : SpaceEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ import io.element.android.libraries.matrix.api.room.join.JoinRoom
|
||||||
import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState
|
import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState
|
||||||
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.ui.safety.rememberHideInvitesAvatar
|
import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
@ -48,6 +49,8 @@ import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableMap
|
import kotlinx.collections.immutable.toImmutableMap
|
||||||
import kotlinx.collections.immutable.toImmutableSet
|
import kotlinx.collections.immutable.toImmutableSet
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.jvm.optionals.getOrNull
|
import kotlin.jvm.optionals.getOrNull
|
||||||
|
|
@ -62,6 +65,7 @@ class SpacePresenter(
|
||||||
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
||||||
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
|
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
|
private val spaceService: SpaceService,
|
||||||
) : Presenter<SpaceState> {
|
) : Presenter<SpaceState> {
|
||||||
private var children by mutableStateOf<ImmutableList<SpaceRoom>>(persistentListOf())
|
private var children by mutableStateOf<ImmutableList<SpaceRoom>>(persistentListOf())
|
||||||
|
|
||||||
|
|
@ -104,6 +108,11 @@ class SpacePresenter(
|
||||||
|
|
||||||
var topicViewerState: TopicViewerState by remember { mutableStateOf(TopicViewerState.Hidden) }
|
var topicViewerState: TopicViewerState by remember { mutableStateOf(TopicViewerState.Hidden) }
|
||||||
|
|
||||||
|
// Manage mode state
|
||||||
|
var isManageMode by remember { mutableStateOf(false) }
|
||||||
|
var selectedRoomIds by remember { mutableStateOf<Set<RoomId>>(emptySet()) }
|
||||||
|
var removeRoomsAction by remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||||
|
|
||||||
LaunchedEffect(children) {
|
LaunchedEffect(children) {
|
||||||
// Remove joined children from the join actions
|
// Remove joined children from the join actions
|
||||||
val joinedChildren = children
|
val joinedChildren = children
|
||||||
|
|
@ -138,6 +147,46 @@ class SpacePresenter(
|
||||||
}
|
}
|
||||||
SpaceEvents.HideTopicViewer -> topicViewerState = TopicViewerState.Hidden
|
SpaceEvents.HideTopicViewer -> topicViewerState = TopicViewerState.Hidden
|
||||||
is SpaceEvents.ShowTopicViewer -> topicViewerState = TopicViewerState.Shown(event.topic)
|
is SpaceEvents.ShowTopicViewer -> topicViewerState = TopicViewerState.Shown(event.topic)
|
||||||
|
|
||||||
|
// Manage mode events
|
||||||
|
SpaceEvents.EnterManageMode -> {
|
||||||
|
isManageMode = true
|
||||||
|
selectedRoomIds = emptySet()
|
||||||
|
}
|
||||||
|
SpaceEvents.ExitManageMode -> {
|
||||||
|
isManageMode = false
|
||||||
|
selectedRoomIds = emptySet()
|
||||||
|
}
|
||||||
|
is SpaceEvents.ToggleRoomSelection -> {
|
||||||
|
selectedRoomIds = if (event.roomId in selectedRoomIds) {
|
||||||
|
selectedRoomIds - event.roomId
|
||||||
|
} else {
|
||||||
|
selectedRoomIds + event.roomId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SpaceEvents.RemoveSelectedRooms -> {
|
||||||
|
removeRoomsAction = AsyncAction.ConfirmingNoParams
|
||||||
|
}
|
||||||
|
SpaceEvents.ConfirmRoomRemoval -> {
|
||||||
|
localCoroutineScope.launch {
|
||||||
|
removeRoomsAction = AsyncAction.Loading
|
||||||
|
val spaceId = spaceRoomList.roomId
|
||||||
|
val results = selectedRoomIds.map { roomId ->
|
||||||
|
async { spaceService.removeChildFromSpace(spaceId, roomId) }
|
||||||
|
}
|
||||||
|
val hasError = results.awaitAll().any { it.isFailure }
|
||||||
|
if (hasError) {
|
||||||
|
removeRoomsAction = AsyncAction.Failure(Exception("Failed to remove some rooms"))
|
||||||
|
} else {
|
||||||
|
removeRoomsAction = AsyncAction.Success(Unit)
|
||||||
|
isManageMode = false
|
||||||
|
selectedRoomIds = emptySet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SpaceEvents.ClearRemoveAction -> {
|
||||||
|
removeRoomsAction = AsyncAction.Uninitialized
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return SpaceState(
|
return SpaceState(
|
||||||
|
|
@ -150,6 +199,10 @@ class SpacePresenter(
|
||||||
acceptDeclineInviteState = acceptDeclineInviteState,
|
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||||
topicViewerState = topicViewerState,
|
topicViewerState = topicViewerState,
|
||||||
canAccessSpaceSettings = canAccessSpaceSettings,
|
canAccessSpaceSettings = canAccessSpaceSettings,
|
||||||
|
isManageMode = isManageMode,
|
||||||
|
selectedRoomIds = selectedRoomIds.toImmutableSet(),
|
||||||
|
canManageRooms = permissions.canManageRooms,
|
||||||
|
removeRoomsAction = removeRoomsAction,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,12 +27,20 @@ data class SpaceState(
|
||||||
val acceptDeclineInviteState: AcceptDeclineInviteState,
|
val acceptDeclineInviteState: AcceptDeclineInviteState,
|
||||||
val topicViewerState: TopicViewerState,
|
val topicViewerState: TopicViewerState,
|
||||||
val canAccessSpaceSettings: Boolean,
|
val canAccessSpaceSettings: Boolean,
|
||||||
|
val isManageMode: Boolean,
|
||||||
|
val selectedRoomIds: ImmutableSet<RoomId>,
|
||||||
|
val canManageRooms: Boolean,
|
||||||
|
val removeRoomsAction: AsyncAction<Unit>,
|
||||||
val eventSink: (SpaceEvents) -> Unit
|
val eventSink: (SpaceEvents) -> Unit
|
||||||
) {
|
) {
|
||||||
fun isJoining(spaceId: RoomId): Boolean = joinActions[spaceId] == AsyncAction.Loading
|
fun isJoining(spaceId: RoomId): Boolean = joinActions[spaceId] == AsyncAction.Loading
|
||||||
val hasAnyFailure: Boolean = joinActions.values.any {
|
val hasAnyFailure: Boolean = joinActions.values.any {
|
||||||
it is AsyncAction.Failure
|
it is AsyncAction.Failure
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val showManageRoomsAction: Boolean = canManageRooms && children.isNotEmpty()
|
||||||
|
val selectedCount: Int = selectedRoomIds.size
|
||||||
|
val isRemoveButtonEnabled: Boolean = selectedRoomIds.isNotEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,26 @@ open class SpaceStateProvider : PreviewParameterProvider<SpaceState> {
|
||||||
aSpaceState(
|
aSpaceState(
|
||||||
topicViewerState = TopicViewerState.Shown(topic = "Space description goes here." + LoremIpsum(20).values.first()),
|
topicViewerState = TopicViewerState.Shown(topic = "Space description goes here." + LoremIpsum(20).values.first()),
|
||||||
),
|
),
|
||||||
// Add other states here
|
// Manage mode states
|
||||||
|
aSpaceState(
|
||||||
|
parentSpace = aParentSpace(),
|
||||||
|
children = aListOfSpaceRooms(),
|
||||||
|
isManageMode = true,
|
||||||
|
selectedRoomIds = emptySet(),
|
||||||
|
),
|
||||||
|
aSpaceState(
|
||||||
|
parentSpace = aParentSpace(),
|
||||||
|
children = aListOfSpaceRooms(),
|
||||||
|
isManageMode = true,
|
||||||
|
selectedRoomIds = setOf(RoomId("!spaceId0:example.com"), RoomId("!spaceId1:example.com")),
|
||||||
|
),
|
||||||
|
aSpaceState(
|
||||||
|
parentSpace = aParentSpace(),
|
||||||
|
children = aListOfSpaceRooms(),
|
||||||
|
isManageMode = true,
|
||||||
|
selectedRoomIds = setOf(RoomId("!spaceId0:example.com")),
|
||||||
|
removeRoomsAction = AsyncAction.ConfirmingNoParams,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,6 +73,10 @@ fun aSpaceState(
|
||||||
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
|
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
|
||||||
topicViewerState: TopicViewerState = TopicViewerState.Hidden,
|
topicViewerState: TopicViewerState = TopicViewerState.Hidden,
|
||||||
canAccessSpaceSettings: Boolean = true,
|
canAccessSpaceSettings: Boolean = true,
|
||||||
|
isManageMode: Boolean = false,
|
||||||
|
selectedRoomIds: Set<RoomId> = emptySet(),
|
||||||
|
canManageRooms: Boolean = true,
|
||||||
|
removeRoomsAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
eventSink: (SpaceEvents) -> Unit = { },
|
eventSink: (SpaceEvents) -> Unit = { },
|
||||||
) = SpaceState(
|
) = SpaceState(
|
||||||
currentSpace = parentSpace,
|
currentSpace = parentSpace,
|
||||||
|
|
@ -65,6 +88,10 @@ fun aSpaceState(
|
||||||
acceptDeclineInviteState = acceptDeclineInviteState,
|
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||||
topicViewerState = topicViewerState,
|
topicViewerState = topicViewerState,
|
||||||
canAccessSpaceSettings = canAccessSpaceSettings,
|
canAccessSpaceSettings = canAccessSpaceSettings,
|
||||||
|
isManageMode = isManageMode,
|
||||||
|
selectedRoomIds = selectedRoomIds.toImmutableSet(),
|
||||||
|
canManageRooms = canManageRooms,
|
||||||
|
removeRoomsAction = removeRoomsAction,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,10 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.InviteButtonsRowMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.InviteButtonsRowMolecule
|
||||||
|
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||||
|
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||||
import io.element.android.libraries.designsystem.components.ClickableLinkText
|
import io.element.android.libraries.designsystem.components.ClickableLinkText
|
||||||
import io.element.android.libraries.designsystem.components.SimpleModalBottomSheet
|
import io.element.android.libraries.designsystem.components.SimpleModalBottomSheet
|
||||||
import io.element.android.libraries.designsystem.components.async.AsyncIndicator
|
import io.element.android.libraries.designsystem.components.async.AsyncIndicator
|
||||||
|
|
@ -56,9 +59,11 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||||
import io.element.android.libraries.designsystem.theme.components.DropdownMenu
|
import io.element.android.libraries.designsystem.theme.components.DropdownMenu
|
||||||
import io.element.android.libraries.designsystem.theme.components.DropdownMenuItem
|
import io.element.android.libraries.designsystem.theme.components.DropdownMenuItem
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Checkbox
|
||||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
|
@ -88,15 +93,26 @@ fun SpaceView(
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
topBar = {
|
topBar = {
|
||||||
SpaceViewTopBar(
|
if (state.isManageMode) {
|
||||||
currentSpace = state.currentSpace,
|
ManageModeTopBar(
|
||||||
canAccessSpaceSettings = state.canAccessSpaceSettings,
|
selectedCount = state.selectedCount,
|
||||||
onBackClick = onBackClick,
|
isRemoveButtonEnabled = state.isRemoveButtonEnabled,
|
||||||
onLeaveSpaceClick = onLeaveSpaceClick,
|
onCancelClick = { state.eventSink(SpaceEvents.ExitManageMode) },
|
||||||
onShareSpace = onShareSpace,
|
onRemoveClick = { state.eventSink(SpaceEvents.RemoveSelectedRooms) },
|
||||||
onSettingsClick = onSettingsClick,
|
)
|
||||||
onViewMembersClick = onViewMembersClick,
|
} else {
|
||||||
)
|
SpaceViewTopBar(
|
||||||
|
currentSpace = state.currentSpace,
|
||||||
|
canAccessSpaceSettings = state.canAccessSpaceSettings,
|
||||||
|
showManageRoomsAction = state.showManageRoomsAction,
|
||||||
|
onBackClick = onBackClick,
|
||||||
|
onLeaveSpaceClick = onLeaveSpaceClick,
|
||||||
|
onShareSpace = onShareSpace,
|
||||||
|
onSettingsClick = onSettingsClick,
|
||||||
|
onViewMembersClick = onViewMembersClick,
|
||||||
|
onManageRoomsClick = { state.eventSink(SpaceEvents.EnterManageMode) },
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
content = { padding ->
|
content = { padding ->
|
||||||
Box(
|
Box(
|
||||||
|
|
@ -104,7 +120,13 @@ fun SpaceView(
|
||||||
) {
|
) {
|
||||||
SpaceViewContent(
|
SpaceViewContent(
|
||||||
state = state,
|
state = state,
|
||||||
onRoomClick = onRoomClick,
|
onRoomClick = { spaceRoom ->
|
||||||
|
if (state.isManageMode) {
|
||||||
|
state.eventSink(SpaceEvents.ToggleRoomSelection(spaceRoom.roomId))
|
||||||
|
} else {
|
||||||
|
onRoomClick(spaceRoom)
|
||||||
|
}
|
||||||
|
},
|
||||||
onTopicClick = { topic ->
|
onTopicClick = { topic ->
|
||||||
state.eventSink(SpaceEvents.ShowTopicViewer(topic))
|
state.eventSink(SpaceEvents.ShowTopicViewer(topic))
|
||||||
}
|
}
|
||||||
|
|
@ -125,6 +147,14 @@ fun SpaceView(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Confirmation dialog for removing rooms
|
||||||
|
RemoveRoomsConfirmationDialog(
|
||||||
|
removeRoomsAction = state.removeRoomsAction,
|
||||||
|
selectedCount = state.selectedCount,
|
||||||
|
onConfirm = { state.eventSink(SpaceEvents.ConfirmRoomRemoval) },
|
||||||
|
onDismiss = { state.eventSink(SpaceEvents.ClearRemoveAction) },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -200,6 +230,7 @@ private fun SpaceViewContent(
|
||||||
) { index, spaceRoom ->
|
) { index, spaceRoom ->
|
||||||
val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED
|
val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED
|
||||||
val isCurrentlyJoining = state.isJoining(spaceRoom.roomId)
|
val isCurrentlyJoining = state.isJoining(spaceRoom.roomId)
|
||||||
|
val isSelected = spaceRoom.roomId in state.selectedRoomIds
|
||||||
SpaceRoomItemView(
|
SpaceRoomItemView(
|
||||||
spaceRoom = spaceRoom,
|
spaceRoom = spaceRoom,
|
||||||
showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites,
|
showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites,
|
||||||
|
|
@ -210,17 +241,30 @@ private fun SpaceViewContent(
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
// TODO
|
// TODO
|
||||||
},
|
},
|
||||||
trailingAction = spaceRoom.trailingAction(isCurrentlyJoining = isCurrentlyJoining) {
|
trailingAction = if (state.isManageMode) {
|
||||||
state.eventSink(SpaceEvents.Join(spaceRoom))
|
{
|
||||||
},
|
Checkbox(
|
||||||
bottomAction = spaceRoom.inviteButtons(
|
checked = isSelected,
|
||||||
onAcceptClick = {
|
onCheckedChange = null,
|
||||||
state.eventSink(SpaceEvents.AcceptInvite(spaceRoom))
|
)
|
||||||
},
|
|
||||||
onDeclineClick = {
|
|
||||||
state.eventSink(SpaceEvents.DeclineInvite(spaceRoom))
|
|
||||||
}
|
}
|
||||||
)
|
} else {
|
||||||
|
spaceRoom.trailingAction(isCurrentlyJoining = isCurrentlyJoining) {
|
||||||
|
state.eventSink(SpaceEvents.Join(spaceRoom))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bottomAction = if (state.isManageMode) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
spaceRoom.inviteButtons(
|
||||||
|
onAcceptClick = {
|
||||||
|
state.eventSink(SpaceEvents.AcceptInvite(spaceRoom))
|
||||||
|
},
|
||||||
|
onDeclineClick = {
|
||||||
|
state.eventSink(SpaceEvents.DeclineInvite(spaceRoom))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
if (index != state.children.lastIndex) {
|
if (index != state.children.lastIndex) {
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
|
|
@ -259,11 +303,13 @@ private fun LoadingMoreIndicator(
|
||||||
private fun SpaceViewTopBar(
|
private fun SpaceViewTopBar(
|
||||||
currentSpace: SpaceRoom?,
|
currentSpace: SpaceRoom?,
|
||||||
canAccessSpaceSettings: Boolean,
|
canAccessSpaceSettings: Boolean,
|
||||||
|
showManageRoomsAction: Boolean,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
onLeaveSpaceClick: () -> Unit,
|
onLeaveSpaceClick: () -> Unit,
|
||||||
onSettingsClick: () -> Unit,
|
onSettingsClick: () -> Unit,
|
||||||
onShareSpace: () -> Unit,
|
onShareSpace: () -> Unit,
|
||||||
onViewMembersClick: () -> Unit,
|
onViewMembersClick: () -> Unit,
|
||||||
|
onManageRoomsClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
|
|
@ -313,6 +359,16 @@ private fun SpaceViewTopBar(
|
||||||
onShareSpace()
|
onShareSpace()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
if (showManageRoomsAction) {
|
||||||
|
SpaceMenuItem(
|
||||||
|
titleRes = CommonStrings.action_manage_rooms,
|
||||||
|
icon = CompoundIcons.Edit(),
|
||||||
|
onClick = {
|
||||||
|
showMenu = false
|
||||||
|
onManageRoomsClick()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
if (canAccessSpaceSettings) {
|
if (canAccessSpaceSettings) {
|
||||||
SpaceMenuItem(
|
SpaceMenuItem(
|
||||||
titleRes = CommonStrings.common_settings,
|
titleRes = CommonStrings.common_settings,
|
||||||
|
|
@ -337,6 +393,39 @@ private fun SpaceViewTopBar(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun ManageModeTopBar(
|
||||||
|
selectedCount: Int,
|
||||||
|
isRemoveButtonEnabled: Boolean,
|
||||||
|
onCancelClick: () -> Unit,
|
||||||
|
onRemoveClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
TopAppBar(
|
||||||
|
modifier = modifier,
|
||||||
|
navigationIcon = {
|
||||||
|
BackButton(
|
||||||
|
onClick = onCancelClick,
|
||||||
|
imageVector = CompoundIcons.Close()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = "$selectedCount selected",
|
||||||
|
style = ElementTheme.typography.fontBodyLgMedium,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
TextButton(
|
||||||
|
text = stringResource(CommonStrings.action_remove),
|
||||||
|
onClick = onRemoveClick,
|
||||||
|
enabled = isRemoveButtonEnabled,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SpaceMenuItem(
|
private fun SpaceMenuItem(
|
||||||
@StringRes titleRes: Int,
|
@StringRes titleRes: Int,
|
||||||
|
|
@ -425,6 +514,34 @@ private fun SpaceRoom.inviteButtons(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RemoveRoomsConfirmationDialog(
|
||||||
|
removeRoomsAction: AsyncAction<Unit>,
|
||||||
|
selectedCount: Int,
|
||||||
|
onConfirm: () -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
) {
|
||||||
|
when (removeRoomsAction) {
|
||||||
|
AsyncAction.ConfirmingNoParams -> {
|
||||||
|
ConfirmationDialog(
|
||||||
|
title = "Remove $selectedCount rooms from space?",
|
||||||
|
content = "Removing a room will not affect the room access. To change the access go to Room info > Privacy & security.",
|
||||||
|
submitText = stringResource(CommonStrings.action_remove),
|
||||||
|
onSubmitClick = onConfirm,
|
||||||
|
onDismiss = onDismiss,
|
||||||
|
destructiveSubmit = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
AsyncActionView(
|
||||||
|
async = removeRoomsAction,
|
||||||
|
onSuccess = { onDismiss() },
|
||||||
|
onErrorDismiss = onDismiss,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SpaceViewPreview(
|
internal fun SpaceViewPreview(
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import io.element.android.features.roomdetailsedit.api.RoomDetailsEditPermission
|
||||||
import io.element.android.features.roomdetailsedit.api.roomDetailsEditPermissions
|
import io.element.android.features.roomdetailsedit.api.roomDetailsEditPermissions
|
||||||
import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyPermissions
|
import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyPermissions
|
||||||
import io.element.android.features.securityandprivacy.api.securityAndPrivacyPermissions
|
import io.element.android.features.securityandprivacy.api.securityAndPrivacyPermissions
|
||||||
|
import io.element.android.libraries.matrix.api.room.StateEventType
|
||||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||||
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPermissions
|
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPermissions
|
||||||
import io.element.android.libraries.matrix.api.room.powerlevels.canEditRolesAndPermissions
|
import io.element.android.libraries.matrix.api.room.powerlevels.canEditRolesAndPermissions
|
||||||
|
|
@ -19,6 +20,7 @@ data class SpaceSettingsPermissions(
|
||||||
val editDetailsPermissions: RoomDetailsEditPermissions,
|
val editDetailsPermissions: RoomDetailsEditPermissions,
|
||||||
val canEditRolesAndPermissions: Boolean,
|
val canEditRolesAndPermissions: Boolean,
|
||||||
val securityAndPrivacyPermissions: SecurityAndPrivacyPermissions,
|
val securityAndPrivacyPermissions: SecurityAndPrivacyPermissions,
|
||||||
|
val canManageRooms: Boolean,
|
||||||
) {
|
) {
|
||||||
fun hasAny(joinRule: JoinRule?): Boolean {
|
fun hasAny(joinRule: JoinRule?): Boolean {
|
||||||
return editDetailsPermissions.hasAny ||
|
return editDetailsPermissions.hasAny ||
|
||||||
|
|
@ -31,6 +33,7 @@ data class SpaceSettingsPermissions(
|
||||||
editDetailsPermissions = RoomDetailsEditPermissions.DEFAULT,
|
editDetailsPermissions = RoomDetailsEditPermissions.DEFAULT,
|
||||||
canEditRolesAndPermissions = false,
|
canEditRolesAndPermissions = false,
|
||||||
securityAndPrivacyPermissions = SecurityAndPrivacyPermissions.DEFAULT,
|
securityAndPrivacyPermissions = SecurityAndPrivacyPermissions.DEFAULT,
|
||||||
|
canManageRooms = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -40,5 +43,6 @@ fun RoomPermissions.spaceSettingsPermissions(): SpaceSettingsPermissions {
|
||||||
editDetailsPermissions = roomDetailsEditPermissions(),
|
editDetailsPermissions = roomDetailsEditPermissions(),
|
||||||
canEditRolesAndPermissions = canEditRolesAndPermissions(),
|
canEditRolesAndPermissions = canEditRolesAndPermissions(),
|
||||||
securityAndPrivacyPermissions = securityAndPrivacyPermissions(),
|
securityAndPrivacyPermissions = securityAndPrivacyPermissions(),
|
||||||
|
canManageRooms = canOwnUserSendState(StateEventType.SpaceChild),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import io.element.android.libraries.matrix.api.room.BaseRoom
|
||||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
import io.element.android.libraries.matrix.api.room.join.JoinRoom
|
import io.element.android.libraries.matrix.api.room.join.JoinRoom
|
||||||
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.test.AN_EXCEPTION
|
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
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||||
|
|
@ -36,6 +37,7 @@ import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
||||||
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
|
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
|
||||||
import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions
|
import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions
|
||||||
import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList
|
import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList
|
||||||
|
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
|
||||||
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||||
import io.element.android.tests.testutils.EventsRecorder
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
|
@ -365,6 +367,7 @@ class SpacePresenterTest {
|
||||||
),
|
),
|
||||||
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
|
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
|
||||||
spaceSettingsEnabled: Boolean = false,
|
spaceSettingsEnabled: Boolean = false,
|
||||||
|
spaceService: FakeSpaceService = FakeSpaceService(),
|
||||||
): SpacePresenter {
|
): SpacePresenter {
|
||||||
return SpacePresenter(
|
return SpacePresenter(
|
||||||
client = client,
|
client = client,
|
||||||
|
|
@ -379,6 +382,7 @@ class SpacePresenterTest {
|
||||||
FeatureFlags.SpaceSettings.key to spaceSettingsEnabled,
|
FeatureFlags.SpaceSettings.key to spaceSettingsEnabled,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
spaceService = spaceService,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,4 +18,12 @@ interface SpaceService {
|
||||||
fun spaceRoomList(id: RoomId): SpaceRoomList
|
fun spaceRoomList(id: RoomId): SpaceRoomList
|
||||||
|
|
||||||
fun getLeaveSpaceHandle(spaceId: RoomId): LeaveSpaceHandle
|
fun getLeaveSpaceHandle(spaceId: RoomId): LeaveSpaceHandle
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a child room from a space.
|
||||||
|
* @param spaceId The space ID from which to remove the child.
|
||||||
|
* @param childId The room ID of the child to remove.
|
||||||
|
* @return A result indicating success or failure.
|
||||||
|
*/
|
||||||
|
suspend fun removeChildFromSpace(spaceId: RoomId, childId: RoomId): Result<Unit>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,12 @@ class RustSpaceService(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun removeChildFromSpace(spaceId: RoomId, childId: RoomId): Result<Unit> = withContext(sessionDispatcher) {
|
||||||
|
runCatchingExceptions {
|
||||||
|
innerSpaceService.removeChildFromSpace(spaceId.value, childId.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
innerSpaceService
|
innerSpaceService
|
||||||
.spaceListUpdate()
|
.spaceListUpdate()
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ 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() },
|
private val leaveSpaceHandleResult: (RoomId) -> LeaveSpaceHandle = { lambdaError() },
|
||||||
|
private val removeChildFromSpaceResult: (RoomId, RoomId) -> Result<Unit> = { _, _ -> 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>>
|
||||||
|
|
@ -43,4 +44,8 @@ class FakeSpaceService(
|
||||||
override fun getLeaveSpaceHandle(spaceId: RoomId): LeaveSpaceHandle {
|
override fun getLeaveSpaceHandle(spaceId: RoomId): LeaveSpaceHandle {
|
||||||
return leaveSpaceHandleResult(spaceId)
|
return leaveSpaceHandleResult(spaceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun removeChildFromSpace(spaceId: RoomId, childId: RoomId): Result<Unit> = simulateLongTask {
|
||||||
|
removeChildFromSpaceResult(spaceId, childId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,7 @@
|
||||||
<string name="action_load_more">"Load more"</string>
|
<string name="action_load_more">"Load more"</string>
|
||||||
<string name="action_manage_account">"Manage account"</string>
|
<string name="action_manage_account">"Manage account"</string>
|
||||||
<string name="action_manage_devices">"Manage devices"</string>
|
<string name="action_manage_devices">"Manage devices"</string>
|
||||||
|
<string name="action_manage_rooms">"Manage rooms"</string>
|
||||||
<string name="action_message">"Message"</string>
|
<string name="action_message">"Message"</string>
|
||||||
<string name="action_minimize">"Minimise"</string>
|
<string name="action_minimize">"Minimise"</string>
|
||||||
<string name="action_next">"Next"</string>
|
<string name="action_next">"Next"</string>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue