Merge pull request #6022 from element-hq/feature/fga/space_manage_rooms

Space : manage rooms
This commit is contained in:
ganfra 2026-01-16 11:54:29 +01:00 committed by GitHub
commit 1070b55bb9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
69 changed files with 894 additions and 153 deletions

View file

@ -8,6 +8,7 @@
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
sealed interface SpaceEvents {
@ -19,4 +20,12 @@ sealed interface SpaceEvents {
data class ShowTopicViewer(val topic: String) : 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
}

View file

@ -0,0 +1,37 @@
/*
* Copyright (c) 2026 Element Creations 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.features.space.impl.root
import io.element.android.features.space.impl.settings.SpaceSettingsPermissions
import io.element.android.features.space.impl.settings.spaceSettingsPermissions
import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPermissions
/**
* Permissions needed for different actions in the Space screen.
* @param settingsPermissions Permissions related to space settings.
* @param canEditSpaceGraph Whether the user can edit the space graph (add/remove children).
*/
data class SpacePermissions(
val settingsPermissions: SpaceSettingsPermissions,
val canEditSpaceGraph: Boolean,
) {
companion object {
val DEFAULT = SpacePermissions(
settingsPermissions = SpaceSettingsPermissions.DEFAULT,
canEditSpaceGraph = false,
)
}
}
fun RoomPermissions.spacePermissions(): SpacePermissions {
return SpacePermissions(
settingsPermissions = spaceSettingsPermissions(),
canEditSpaceGraph = canOwnUserSendState(StateEventType.SpaceChild) || canOwnUserSendState(StateEventType.SpaceParent),
)
}

View file

@ -23,8 +23,6 @@ import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.invite.api.toInviteData
import io.element.android.features.space.impl.settings.SpaceSettingsPermissions
import io.element.android.features.space.impl.settings.spaceSettingsPermissions
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.coroutine.mapState
@ -40,6 +38,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.spaces.SpaceRoom
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 kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -48,9 +47,10 @@ import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlin.jvm.optionals.getOrNull
@Inject
class SpacePresenter(
@ -62,6 +62,7 @@ class SpacePresenter(
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
private val featureFlagService: FeatureFlagService,
private val spaceService: SpaceService,
) : Presenter<SpaceState> {
private var children by mutableStateOf<ImmutableList<SpaceRoom>>(persistentListOf())
@ -88,8 +89,8 @@ class SpacePresenter(
}
}.collectAsState()
val permissions by room.permissionsAsState(SpaceSettingsPermissions.DEFAULT) { perms ->
perms.spaceSettingsPermissions()
val permissions by room.permissionsAsState(SpacePermissions.DEFAULT) { perms ->
perms.spacePermissions()
}
val isSpaceSettingsEnabled by remember {
featureFlagService.isFeatureEnabledFlow(FeatureFlags.SpaceSettings)
@ -97,13 +98,37 @@ class SpacePresenter(
val roomInfo by room.roomInfoFlow.collectAsState()
val canAccessSpaceSettings by remember {
derivedStateOf { isSpaceSettingsEnabled && permissions.hasAny(roomInfo.joinRule) }
derivedStateOf { isSpaceSettingsEnabled && permissions.settingsPermissions.hasAny(roomInfo.joinRule) }
}
val canEditSpaceGraph by remember {
derivedStateOf { isSpaceSettingsEnabled && permissions.canEditSpaceGraph }
}
val currentSpace by spaceRoomList.currentSpaceFlow.collectAsState()
val (joinActions, setJoinActions) = remember { mutableStateOf(emptyMap<RoomId, AsyncAction<Unit>>()) }
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) }
var removedRoomIds by remember { mutableStateOf<Set<RoomId>>(emptySet()) }
val filteredChildren by remember {
derivedStateOf {
children
.filterNot { it.roomId in removedRoomIds }
.let { list ->
if (isManageMode) {
// In manage mode, only show rooms (not spaces)
list.filter { !it.isSpace }
} else {
list
}
}
.toImmutableList()
}
}
LaunchedEffect(children) {
// Remove joined children from the join actions
val joinedChildren = children
@ -138,11 +163,60 @@ class SpacePresenter(
}
SpaceEvents.HideTopicViewer -> topicViewerState = TopicViewerState.Hidden
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 roomsToRemove = selectedRoomIds.toSet()
val successfullyRemoved = mutableSetOf<RoomId>()
val results = roomsToRemove.map { roomId ->
async {
spaceService.removeChildFromSpace(spaceId, roomId)
.onSuccess { successfullyRemoved.add(roomId) }
}
}
results.awaitAll()
if (successfullyRemoved.isNotEmpty()) {
removedRoomIds = removedRoomIds + successfullyRemoved
}
val hasError = successfullyRemoved.size < roomsToRemove.size
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(
currentSpace = currentSpace.getOrNull(),
children = children,
spaceInfo = roomInfo,
children = filteredChildren,
seenSpaceInvites = seenSpaceInvites,
hideInvitesAvatar = hideInvitesAvatar,
hasMoreToLoad = hasMoreToLoad,
@ -150,6 +224,10 @@ class SpacePresenter(
acceptDeclineInviteState = acceptDeclineInviteState,
topicViewerState = topicViewerState,
canAccessSpaceSettings = canAccessSpaceSettings,
isManageMode = isManageMode,
selectedRoomIds = selectedRoomIds.toImmutableSet(),
canEditSpaceGraph = canEditSpaceGraph,
removeRoomsAction = removeRoomsAction,
eventSink = ::handleEvent,
)
}

View file

@ -12,13 +12,14 @@ import androidx.compose.runtime.Immutable
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.ImmutableSet
data class SpaceState(
val currentSpace: SpaceRoom?,
val spaceInfo: RoomInfo,
val children: ImmutableList<SpaceRoom>,
val seenSpaceInvites: ImmutableSet<RoomId>,
val hideInvitesAvatar: Boolean,
@ -27,12 +28,21 @@ data class SpaceState(
val acceptDeclineInviteState: AcceptDeclineInviteState,
val topicViewerState: TopicViewerState,
val canAccessSpaceSettings: Boolean,
val isManageMode: Boolean,
val selectedRoomIds: ImmutableSet<RoomId>,
val canEditSpaceGraph: Boolean,
val removeRoomsAction: AsyncAction<Unit>,
val eventSink: (SpaceEvents) -> Unit
) {
fun isJoining(spaceId: RoomId): Boolean = joinActions[spaceId] == AsyncAction.Loading
val hasAnyFailure: Boolean = joinActions.values.any {
fun isSelected(spaceId: RoomId): Boolean = selectedRoomIds.contains(spaceId)
val hasAnyJoinFailures: Boolean = joinActions.values.any {
it is AsyncAction.Failure
}
val showManageRoomsAction: Boolean = canEditSpaceGraph && children.any { spaceRoom -> !spaceRoom.isSpace }
val selectedCount: Int = selectedRoomIds.size
val isRemoveButtonEnabled: Boolean = selectedRoomIds.isNotEmpty()
}
@Immutable

View file

@ -15,6 +15,8 @@ import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInvit
import io.element.android.libraries.architecture.AsyncAction
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.RoomInfo
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.previewutils.room.aSpaceRoom
@ -27,11 +29,11 @@ open class SpaceStateProvider : PreviewParameterProvider<SpaceState> {
override val values: Sequence<SpaceState>
get() = sequenceOf(
aSpaceState(),
aSpaceState(parentSpace = aParentSpace(joinRule = JoinRule.Public)),
aSpaceState(parentSpace = aParentSpace(joinRule = JoinRule.Restricted(persistentListOf()))),
aSpaceState(spaceInfo = aSpaceInfo(joinRule = JoinRule.Public)),
aSpaceState(spaceInfo = aSpaceInfo(joinRule = JoinRule.Restricted(persistentListOf()))),
aSpaceState(children = aListOfSpaceRooms()),
aSpaceState(
parentSpace = aParentSpace(),
spaceInfo = aSpaceInfo(),
children = aListOfSpaceRooms(),
joiningRooms = setOf(RoomId("!spaceId0:example.com")),
hasMoreToLoad = false
@ -39,12 +41,31 @@ open class SpaceStateProvider : PreviewParameterProvider<SpaceState> {
aSpaceState(
topicViewerState = TopicViewerState.Shown(topic = "Space description goes here." + LoremIpsum(20).values.first()),
),
// Add other states here
// Manage mode states
aSpaceState(
spaceInfo = aSpaceInfo(),
children = aListOfSpaceRooms(),
isManageMode = true,
selectedRoomIds = emptySet(),
),
aSpaceState(
spaceInfo = aSpaceInfo(),
children = aListOfSpaceRooms(),
isManageMode = true,
selectedRoomIds = setOf(RoomId("!spaceId0:example.com"), RoomId("!spaceId1:example.com")),
),
aSpaceState(
spaceInfo = aSpaceInfo(),
children = aListOfSpaceRooms(),
isManageMode = true,
selectedRoomIds = setOf(RoomId("!spaceId0:example.com")),
removeRoomsAction = AsyncAction.ConfirmingNoParams,
),
)
}
fun aSpaceState(
parentSpace: SpaceRoom? = aParentSpace(),
spaceInfo: RoomInfo = aSpaceInfo(),
children: List<SpaceRoom> = emptyList(),
seenSpaceInvites: Set<RoomId> = emptySet(),
joiningRooms: Set<RoomId> = emptySet(),
@ -54,9 +75,13 @@ fun aSpaceState(
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
topicViewerState: TopicViewerState = TopicViewerState.Hidden,
canAccessSpaceSettings: Boolean = true,
isManageMode: Boolean = false,
selectedRoomIds: Set<RoomId> = emptySet(),
canManageRooms: Boolean = true,
removeRoomsAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
eventSink: (SpaceEvents) -> Unit = { },
) = SpaceState(
currentSpace = parentSpace,
spaceInfo = spaceInfo,
children = children.toImmutableList(),
seenSpaceInvites = seenSpaceInvites.toImmutableSet(),
hideInvitesAvatar = hideInvitesAvatar,
@ -65,19 +90,52 @@ fun aSpaceState(
acceptDeclineInviteState = acceptDeclineInviteState,
topicViewerState = topicViewerState,
canAccessSpaceSettings = canAccessSpaceSettings,
isManageMode = isManageMode,
selectedRoomIds = selectedRoomIds.toImmutableSet(),
canEditSpaceGraph = canManageRooms,
removeRoomsAction = removeRoomsAction,
eventSink = eventSink,
)
private fun aParentSpace(
private fun aSpaceInfo(
joinRule: JoinRule? = null,
): SpaceRoom {
return aSpaceRoom(
numJoinedMembers = 5,
childrenCount = 10,
worldReadable = true,
joinRule = joinRule,
roomId = RoomId("!spaceId0:example.com"),
): RoomInfo {
return RoomInfo(
id = RoomId("!spaceId0:example.com"),
name = "A Space",
rawName = "A Space",
topic = "Space description goes here. " + LoremIpsum(20).values.first(),
avatarUrl = null,
isPublic = true,
isDirect = false,
isEncrypted = false,
joinRule = joinRule,
isSpace = true,
isFavorite = false,
canonicalAlias = null,
alternativeAliases = persistentListOf(),
currentUserMembership = CurrentUserMembership.JOINED,
inviter = null,
activeMembersCount = 5,
invitedMembersCount = 0,
joinedMembersCount = 5,
roomPowerLevels = null,
highlightCount = 0,
notificationCount = 0,
userDefinedNotificationMode = null,
hasRoomCall = false,
activeRoomCallParticipants = persistentListOf(),
isMarkedUnread = false,
numUnreadMessages = 0,
numUnreadNotifications = 0,
numUnreadMentions = 0,
heroes = persistentListOf(),
pinnedEventIds = persistentListOf(),
creators = persistentListOf(),
historyVisibility = RoomHistoryVisibility.Joined,
successorRoom = null,
roomVersion = "11",
privilegedCreatorRole = false,
)
}

View file

@ -8,10 +8,17 @@
package io.element.android.features.space.impl.root
import androidx.activity.compose.BackHandler
import androidx.annotation.StringRes
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@ -31,6 +38,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics
@ -40,9 +48,12 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.space.impl.R
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.atomic.molecules.InviteButtonsRowMolecule
import io.element.android.libraries.designsystem.components.ClickableLinkText
import io.element.android.libraries.designsystem.components.SimpleModalBottomSheet
import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.async.AsyncIndicator
import io.element.android.libraries.designsystem.components.async.AsyncIndicatorHost
import io.element.android.libraries.designsystem.components.async.rememberAsyncIndicatorState
@ -51,8 +62,10 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Checkbox
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.DropdownMenuItem
@ -61,15 +74,18 @@ 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.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.matrix.api.spaces.SpaceRoomVisibility
import io.element.android.libraries.matrix.ui.components.JoinButton
import io.element.android.libraries.matrix.ui.components.SpaceHeaderView
import io.element.android.libraries.matrix.ui.components.SpaceRoomItemView
import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.ui.strings.CommonPlurals
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay
@OptIn(ExperimentalMaterial3Api::class)
@ -85,18 +101,48 @@ fun SpaceView(
modifier: Modifier = Modifier,
acceptDeclineInviteView: @Composable () -> Unit,
) {
BackHandler {
if (state.isManageMode) {
state.eventSink(SpaceEvents.ExitManageMode)
} else {
onBackClick()
}
}
Scaffold(
modifier = modifier,
topBar = {
SpaceViewTopBar(
currentSpace = state.currentSpace,
canAccessSpaceSettings = state.canAccessSpaceSettings,
onBackClick = onBackClick,
onLeaveSpaceClick = onLeaveSpaceClick,
onShareSpace = onShareSpace,
onSettingsClick = onSettingsClick,
onViewMembersClick = onViewMembersClick,
)
Box {
AnimatedVisibility(
visible = state.isManageMode,
enter = fadeIn(),
exit = fadeOut()
) {
ManageModeTopBar(
selectedCount = state.selectedCount,
isRemoveButtonEnabled = state.isRemoveButtonEnabled,
onCancelClick = { state.eventSink(SpaceEvents.ExitManageMode) },
onRemoveClick = { state.eventSink(SpaceEvents.RemoveSelectedRooms) },
)
}
AnimatedVisibility(
visible = !state.isManageMode,
enter = fadeIn(),
exit = fadeOut()
) {
SpaceViewTopBar(
spaceInfo = state.spaceInfo,
canAccessSpaceSettings = state.canAccessSpaceSettings,
showManageRoomsAction = state.showManageRoomsAction,
onBackClick = onBackClick,
onLeaveSpaceClick = onLeaveSpaceClick,
onSettingsClick = onSettingsClick,
onShareSpace = onShareSpace,
onViewMembersClick = onViewMembersClick,
onManageRoomsClick = { state.eventSink(SpaceEvents.EnterManageMode) },
)
}
}
},
content = { padding ->
Box(
@ -104,15 +150,28 @@ fun SpaceView(
) {
SpaceViewContent(
state = state,
onRoomClick = onRoomClick,
onRoomClick = { spaceRoom ->
if (state.isManageMode) {
state.eventSink(SpaceEvents.ToggleRoomSelection(spaceRoom.roomId))
} else {
onRoomClick(spaceRoom)
}
},
onTopicClick = { topic ->
state.eventSink(SpaceEvents.ShowTopicViewer(topic))
}
)
JoinRoomFailureEffect(
hasAnyFailure = state.hasAnyFailure,
JoinFailuresEffect(
hasAnyFailure = state.hasAnyJoinFailures,
eventSink = state.eventSink
)
RemoveRoomsActionView(
spaceDisplayName = state.spaceInfo.name ?: state.spaceInfo.id.value,
removeRoomsAction = state.removeRoomsAction,
selectedCount = state.selectedCount,
onConfirm = { state.eventSink(SpaceEvents.ConfirmRoomRemoval) },
onDismiss = { state.eventSink(SpaceEvents.ClearRemoveAction) },
)
acceptDeclineInviteView()
}
},
@ -128,7 +187,7 @@ fun SpaceView(
}
@Composable
private fun JoinRoomFailureEffect(
private fun JoinFailuresEffect(
hasAnyFailure: Boolean,
eventSink: (SpaceEvents) -> Unit,
) {
@ -176,22 +235,26 @@ private fun SpaceViewContent(
modifier: Modifier = Modifier,
) {
LazyColumn(modifier.fillMaxSize()) {
val currentSpace = state.currentSpace
if (currentSpace != null) {
item {
SpaceHeaderView(
avatarData = currentSpace.getAvatarData(AvatarSize.SpaceHeader),
name = currentSpace.displayName,
topic = currentSpace.topic,
topicMaxLines = 2,
visibility = currentSpace.visibility,
heroes = currentSpace.heroes.toImmutableList(),
numberOfMembers = currentSpace.numJoinedMembers,
onTopicClick = onTopicClick
)
}
item {
HorizontalDivider()
val spaceInfo = state.spaceInfo
item(key = "space_header") {
AnimatedVisibility(
!state.isManageMode,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
Column {
SpaceHeaderView(
avatarData = spaceInfo.getAvatarData(AvatarSize.SpaceHeader),
name = spaceInfo.name,
topic = spaceInfo.topic,
topicMaxLines = 2,
visibility = SpaceRoomVisibility.fromJoinRule(spaceInfo.joinRule),
heroes = spaceInfo.heroes,
numberOfMembers = spaceInfo.joinedMembersCount.toInt(),
onTopicClick = onTopicClick
)
HorizontalDivider()
}
}
}
itemsIndexed(
@ -200,9 +263,11 @@ private fun SpaceViewContent(
) { index, spaceRoom ->
val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED
val isCurrentlyJoining = state.isJoining(spaceRoom.roomId)
val isSelected = state.isSelected(spaceRoom.roomId)
val showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites && !state.isManageMode
SpaceRoomItemView(
spaceRoom = spaceRoom,
showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites,
showUnreadIndicator = showUnreadIndicator,
hideAvatars = isInvitation && state.hideInvitesAvatar,
onClick = {
onRoomClick(spaceRoom)
@ -210,17 +275,30 @@ private fun SpaceViewContent(
onLongClick = {
// TODO
},
trailingAction = spaceRoom.trailingAction(isCurrentlyJoining = isCurrentlyJoining) {
state.eventSink(SpaceEvents.Join(spaceRoom))
},
bottomAction = spaceRoom.inviteButtons(
onAcceptClick = {
state.eventSink(SpaceEvents.AcceptInvite(spaceRoom))
},
onDeclineClick = {
state.eventSink(SpaceEvents.DeclineInvite(spaceRoom))
trailingAction = if (state.isManageMode) {
{
Checkbox(
checked = isSelected,
onCheckedChange = null,
)
}
)
} 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) {
HorizontalDivider()
@ -257,13 +335,15 @@ private fun LoadingMoreIndicator(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun SpaceViewTopBar(
currentSpace: SpaceRoom?,
spaceInfo: RoomInfo,
canAccessSpaceSettings: Boolean,
showManageRoomsAction: Boolean,
onBackClick: () -> Unit,
onLeaveSpaceClick: () -> Unit,
onSettingsClick: () -> Unit,
onShareSpace: () -> Unit,
onViewMembersClick: () -> Unit,
onManageRoomsClick: () -> Unit,
modifier: Modifier = Modifier,
) {
TopAppBar(
@ -272,16 +352,14 @@ private fun SpaceViewTopBar(
BackButton(onClick = onBackClick)
},
title = {
if (currentSpace != null) {
val roundedCornerShape = RoundedCornerShape(8.dp)
SpaceAvatarAndNameRow(
name = currentSpace.displayName,
avatarData = currentSpace.getAvatarData(AvatarSize.TimelineRoom),
modifier = Modifier
.clip(roundedCornerShape)
.clickable(enabled = canAccessSpaceSettings, onClick = onSettingsClick)
)
}
val roundedCornerShape = RoundedCornerShape(8.dp)
SpaceAvatarAndNameRow(
name = spaceInfo.name,
avatarData = spaceInfo.getAvatarData(AvatarSize.TimelineRoom),
modifier = Modifier
.clip(roundedCornerShape)
.clickable(enabled = canAccessSpaceSettings, onClick = onSettingsClick)
)
},
actions = {
var showMenu by remember { mutableStateOf(false) }
@ -297,8 +375,19 @@ private fun SpaceViewTopBar(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
if (showManageRoomsAction) {
SpaceMenuItem(
titleRes = CommonStrings.action_manage_rooms,
icon = CompoundIcons.Edit(),
onClick = {
showMenu = false
onManageRoomsClick()
}
)
HorizontalDivider()
}
SpaceMenuItem(
titleRes = CommonStrings.screen_space_menu_action_members,
titleRes = R.string.screen_space_menu_action_members,
icon = CompoundIcons.User(),
onClick = {
showMenu = false
@ -323,6 +412,7 @@ private fun SpaceViewTopBar(
}
)
}
HorizontalDivider()
SpaceMenuItem(
titleRes = CommonStrings.action_leave_space,
icon = CompoundIcons.Leave(),
@ -337,6 +427,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 = pluralStringResource(CommonPlurals.common_selected_count, selectedCount, selectedCount),
style = ElementTheme.typography.fontBodyLgMedium,
)
},
actions = {
TextButton(
text = stringResource(CommonStrings.action_remove),
onClick = onRemoveClick,
enabled = isRemoveButtonEnabled,
)
},
)
}
@Composable
private fun SpaceMenuItem(
@StringRes titleRes: Int,
@ -425,6 +548,45 @@ private fun SpaceRoom.inviteButtons(
}
}
@Composable
private fun RemoveRoomsActionView(
spaceDisplayName: String,
removeRoomsAction: AsyncAction<Unit>,
selectedCount: Int,
onConfirm: () -> Unit,
onDismiss: () -> Unit,
) {
AsyncActionView(
async = removeRoomsAction,
confirmationDialog = {
ConfirmationDialog(
title = pluralStringResource(R.plurals.screen_space_remove_rooms_confirmation_title, selectedCount, selectedCount, spaceDisplayName),
content = stringResource(R.string.screen_space_remove_rooms_confirmation_content),
submitText = stringResource(CommonStrings.action_remove),
onSubmitClick = onConfirm,
onDismiss = onDismiss,
destructiveSubmit = true,
icon = {
Icon(
imageVector = CompoundIcons.Error(),
tint = ElementTheme.colors.textCriticalPrimary,
contentDescription = null
)
}
)
},
onRetry = onConfirm,
errorTitle = {
stringResource(CommonStrings.common_something_went_wrong)
},
errorMessage = {
stringResource(CommonStrings.error_network_or_server_issue)
},
onSuccess = { onDismiss() },
onErrorDismiss = onDismiss,
)
}
@PreviewsDayNight
@Composable
internal fun SpaceViewPreview(

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_space_menu_action_members">"Преглед на членовете"</string>
<string name="screen_space_settings_leave_space">"Напускане на пространството"</string>
<string name="screen_space_settings_roles_and_permissions">"Роли и разрешения"</string>
<string name="screen_space_settings_security_and_privacy">"Защита и поверителност"</string>

View file

@ -11,6 +11,7 @@
<string name="screen_leave_space_subtitle_only_last_admin">"Z následujících místností nebudete odstraněni, protože jste jediným administrátorem:"</string>
<string name="screen_leave_space_title">"Opustit %1$s?"</string>
<string name="screen_leave_space_title_last_admin">"Jste jediným administrátorem pro %1$s"</string>
<string name="screen_space_menu_action_members">"Zobrazit členy"</string>
<string name="screen_space_settings_leave_space">"Opustit prostor"</string>
<string name="screen_space_settings_roles_and_permissions">"Role a oprávnění"</string>
<string name="screen_space_settings_security_and_privacy">"Zabezpečení a soukromí"</string>

View file

@ -10,6 +10,7 @@
<string name="screen_leave_space_subtitle_only_last_admin">"Du vil ikke blive fjernet fra følgende rum, fordi du er den eneste administrator:"</string>
<string name="screen_leave_space_title">"Forlad %1$s?"</string>
<string name="screen_leave_space_title_last_admin">"Du er den eneste administrator for %1$s"</string>
<string name="screen_space_menu_action_members">"Vis medlemmer"</string>
<string name="screen_space_settings_leave_space">"Forlad gruppe"</string>
<string name="screen_space_settings_roles_and_permissions">"Roller og tilladelser"</string>
<string name="screen_space_settings_security_and_privacy">"Sikkerhed og privatliv"</string>

View file

@ -10,6 +10,12 @@
<string name="screen_leave_space_subtitle_only_last_admin">"Du wirst aus den folgenden Chats nicht entfernt, weil du der einzige Admin bist:"</string>
<string name="screen_leave_space_title">"%1$s verlassen?"</string>
<string name="screen_leave_space_title_last_admin">"Du bist der einzige Administrator für %1$s"</string>
<string name="screen_space_menu_action_members">"Mitglieder anzeigen"</string>
<string name="screen_space_remove_rooms_confirmation_content">"Das Entfernen eines Chats hat keinen Einfluss auf die Beitrittsregeln. Um die Regeln zu ändern, gehe zu \"Raum Info\" und dann zu \"Datenschutz und Sicherheit\""</string>
<plurals name="screen_space_remove_rooms_confirmation_title">
<item quantity="one">"%1$d chat aus %2$s entfernen"</item>
<item quantity="other">"%1$d chats aus %2$s entfernen"</item>
</plurals>
<string name="screen_space_settings_leave_space">"Space verlassen"</string>
<string name="screen_space_settings_roles_and_permissions">"Rollen und Berechtigungen"</string>
<string name="screen_space_settings_security_and_privacy">"Sicherheit &amp; Datenschutz"</string>

View file

@ -10,6 +10,7 @@
<string name="screen_leave_space_subtitle_only_last_admin">"Sind ei saa järgnevatest jututubadest eemaldada, kuna oled seal/neis ainus peakasutaja:"</string>
<string name="screen_leave_space_title">"Kas lahkud %1$s kogukonnast?"</string>
<string name="screen_leave_space_title_last_admin">"Sa oled siin ainus peakasutaja: %1$s"</string>
<string name="screen_space_menu_action_members">"Vaata liikmeid"</string>
<string name="screen_space_settings_leave_space">"Lahku kogukonnast"</string>
<string name="screen_space_settings_roles_and_permissions">"Rollid ja õigused"</string>
<string name="screen_space_settings_security_and_privacy">"Turvalisus ja privaatsus"</string>

View file

@ -5,6 +5,7 @@
<string name="screen_leave_space_subtitle_only_last_admin">"از اتاق(های) زیر برداشته نخواهید شد؛ چرا که تنها مدیر هستید:"</string>
<string name="screen_leave_space_title">"ترک %1$s؟"</string>
<string name="screen_leave_space_title_last_admin">"تنها مدیر %1$s هستید"</string>
<string name="screen_space_menu_action_members">"دیدن اعضا"</string>
<string name="screen_space_settings_leave_space">"ترک فضا"</string>
<string name="screen_space_settings_roles_and_permissions">"نقش‌ها و اجازه‌ها"</string>
<string name="screen_space_settings_security_and_privacy">"امنیت و محرمانگی"</string>

View file

@ -10,6 +10,7 @@
<string name="screen_leave_space_subtitle_only_last_admin">"Sinua ei poisteta seuraavista huoneista, koska olet ainoa ylläpitäjä:"</string>
<string name="screen_leave_space_title">"Haluatko poistua tilasta %1$s?"</string>
<string name="screen_leave_space_title_last_admin">"Olet ainoa ylläpitäjä tilassa %1$s"</string>
<string name="screen_space_menu_action_members">"Näytä jäsenet"</string>
<string name="screen_space_settings_leave_space">"Poistu tilasta"</string>
<string name="screen_space_settings_roles_and_permissions">"Roolit ja oikeudet"</string>
<string name="screen_space_settings_security_and_privacy">"Turvallisuus ja yksityisyys"</string>

View file

@ -10,6 +10,7 @@
<string name="screen_leave_space_subtitle_only_last_admin">"Vous ne quitterez pas le ou les salons suivants car vous y êtes le seul administrateur:"</string>
<string name="screen_leave_space_title">"Quitter %1$s?"</string>
<string name="screen_leave_space_title_last_admin">"Vous êtes le seul administrateur de %1$s"</string>
<string name="screen_space_menu_action_members">"Voir les membres"</string>
<string name="screen_space_settings_leave_space">"Quitter lespace"</string>
<string name="screen_space_settings_roles_and_permissions">"Rôles &amp; autorisations"</string>
<string name="screen_space_settings_security_and_privacy">"Sécurité &amp; confidentialité"</string>

View file

@ -11,6 +11,7 @@
<string name="screen_leave_space_subtitle_only_last_admin">"Nećete biti uklonjeni iz sljedećih soba jer ste jedini administrator:"</string>
<string name="screen_leave_space_title">"Želite li napustiti %1$s?"</string>
<string name="screen_leave_space_title_last_admin">"Vi ste jedini administrator za %1$s"</string>
<string name="screen_space_menu_action_members">"Prikaži članove"</string>
<string name="screen_space_settings_leave_space">"Napusti prostor"</string>
<string name="screen_space_settings_roles_and_permissions">"Uloge i dopuštenja"</string>
<string name="screen_space_settings_security_and_privacy">"Sigurnost i privatnost"</string>

View file

@ -10,6 +10,7 @@
<string name="screen_leave_space_subtitle_only_last_admin">"Nem lesz eltávolítva a következő szobá(k)ból, mert ön az egyetlen adminisztrátor:"</string>
<string name="screen_leave_space_title">"Kilép innen: %1$s?"</string>
<string name="screen_leave_space_title_last_admin">"Ön az egyetlen adminisztrátor itt: %1$s"</string>
<string name="screen_space_menu_action_members">"Tagok megtekintése"</string>
<string name="screen_space_settings_leave_space">"Tér elhagyása"</string>
<string name="screen_space_settings_roles_and_permissions">"Szerepkörök és jogosultságok"</string>
<string name="screen_space_settings_security_and_privacy">"Biztonság és adatvédelem"</string>

View file

@ -10,6 +10,7 @@
<string name="screen_leave_space_subtitle_only_last_admin">"Non verrai rimosso dalle seguenti stanze perché sei l\'unico amministratore:"</string>
<string name="screen_leave_space_title">"Uscire da %1$s?"</string>
<string name="screen_leave_space_title_last_admin">"Sei l\'unico amministratore di %1$s"</string>
<string name="screen_space_menu_action_members">"Visualizza membri"</string>
<string name="screen_space_settings_leave_space">"Esci dallo spazio"</string>
<string name="screen_space_settings_roles_and_permissions">"Ruoli e autorizzazioni"</string>
<string name="screen_space_settings_security_and_privacy">"Sicurezza e privacy"</string>

View file

@ -10,6 +10,7 @@
<string name="screen_leave_space_subtitle_only_last_admin">"Du vil ikke bli fjernet fra følgende rom fordi du er den eneste administratoren:"</string>
<string name="screen_leave_space_title">"Forlat %1$s?"</string>
<string name="screen_leave_space_title_last_admin">"Du er den eneste administratoren for %1$s"</string>
<string name="screen_space_menu_action_members">"Vis medlemmer"</string>
<string name="screen_space_settings_leave_space">"Forlat område"</string>
<string name="screen_space_settings_roles_and_permissions">"Roller og tillatelser"</string>
<string name="screen_space_settings_security_and_privacy">"Sikkerhet og personvern"</string>

View file

@ -10,6 +10,7 @@
<string name="screen_leave_space_subtitle_only_last_admin">"Você não será removido das seguintes salas porque você é o único administrador:"</string>
<string name="screen_leave_space_title">"Sair de %1$s?"</string>
<string name="screen_leave_space_title_last_admin">"Você é o único administrador de %1$s"</string>
<string name="screen_space_menu_action_members">"Ver membros"</string>
<string name="screen_space_settings_leave_space">"Sair do espaço"</string>
<string name="screen_space_settings_roles_and_permissions">"Cargos e permissões"</string>
<string name="screen_space_settings_security_and_privacy">"Segurança e privacidade"</string>

View file

@ -11,6 +11,7 @@
<string name="screen_leave_space_subtitle_only_last_admin">"Nu veți părăsi următoarele camere deoarece sunteți singurul administrator:"</string>
<string name="screen_leave_space_title">"Părăsiți %1$s?"</string>
<string name="screen_leave_space_title_last_admin">"Sunteți singurul administrator pentru %1$s"</string>
<string name="screen_space_menu_action_members">"Vizualizați membrii"</string>
<string name="screen_space_settings_leave_space">"Părăsiți spațiul"</string>
<string name="screen_space_settings_roles_and_permissions">"Roluri și permisiuni"</string>
<string name="screen_space_settings_security_and_privacy">"Securitate &amp; confidențialitate"</string>

View file

@ -11,6 +11,7 @@
<string name="screen_leave_space_subtitle_only_last_admin">"Вы не будете удалены из следующих комнат, поскольку вы являетесь единственным администратором:"</string>
<string name="screen_leave_space_title">"Выйти из %1$s?"</string>
<string name="screen_leave_space_title_last_admin">"Вы единственный администратор для %1$s"</string>
<string name="screen_space_menu_action_members">"Просмотреть участников"</string>
<string name="screen_space_settings_leave_space">"Покинуть пространство"</string>
<string name="screen_space_settings_roles_and_permissions">"Роли и разрешения"</string>
<string name="screen_space_settings_security_and_privacy">"Безопасность и конфиденциальность"</string>

View file

@ -11,6 +11,7 @@
<string name="screen_leave_space_subtitle_only_last_admin">"Z nasledujúcich miestností nebudete odstránený/á, pretože ste jediným správcom:"</string>
<string name="screen_leave_space_title">"Opustiť %1$s?"</string>
<string name="screen_leave_space_title_last_admin">"Ste jediným administrátorom pre %1$s"</string>
<string name="screen_space_menu_action_members">"Zobraziť členov"</string>
<string name="screen_space_settings_leave_space">"Opustiť priestor"</string>
<string name="screen_space_settings_roles_and_permissions">"Roly a povolenia"</string>
<string name="screen_space_settings_security_and_privacy">"Bezpečnosť a súkromie"</string>

View file

@ -9,6 +9,7 @@
<string name="screen_leave_space_subtitle_only_last_admin">"您不會被從以下聊天室移除,因為您是唯一的管理員:"</string>
<string name="screen_leave_space_title">"離開 %1$s"</string>
<string name="screen_leave_space_title_last_admin">"您是 %1$s 唯一的管理員"</string>
<string name="screen_space_menu_action_members">"檢視成員"</string>
<string name="screen_space_settings_leave_space">"離開空間"</string>
<string name="screen_space_settings_roles_and_permissions">"角色與權限"</string>
<string name="screen_space_settings_security_and_privacy">"安全與隱私"</string>

View file

@ -9,6 +9,7 @@
<string name="screen_leave_space_subtitle_only_last_admin">"您不会从以下房间中被移除,因为您是唯一的管理员:"</string>
<string name="screen_leave_space_title">"离开%1$s"</string>
<string name="screen_leave_space_title_last_admin">"您是 %1$s 的唯一管理员"</string>
<string name="screen_space_menu_action_members">"查看成员"</string>
<string name="screen_space_settings_leave_space">"离开空间"</string>
<string name="screen_space_settings_roles_and_permissions">"角色与权限"</string>
<string name="screen_space_settings_security_and_privacy">"安全与隐私"</string>

View file

@ -10,6 +10,12 @@
<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>
<string name="screen_space_menu_action_members">"View members"</string>
<string name="screen_space_remove_rooms_confirmation_content">"Removing a room will not affect the room access. To change the access go to Room info &gt; Privacy &amp; security."</string>
<plurals name="screen_space_remove_rooms_confirmation_title">
<item quantity="one">"Remove %1$d room from %2$s"</item>
<item quantity="other">"Remove %1$d rooms from %2$s"</item>
</plurals>
<string name="screen_space_settings_leave_space">"Leave space"</string>
<string name="screen_space_settings_roles_and_permissions">"Roles &amp; permissions"</string>
<string name="screen_space_settings_security_and_privacy">"Security &amp; privacy"</string>

View file

@ -22,20 +22,24 @@ import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
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.RoomType
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.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_ROOM_ID_3
import io.element.android.libraries.matrix.test.FakeMatrixClient
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.powerlevels.FakeRoomPermissions
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.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.lambda.lambdaRecorder
@ -59,7 +63,7 @@ class SpacePresenterTest {
val presenter = createSpacePresenter(spaceRoomList = spaceRoomList)
presenter.test {
val state = awaitItem()
assertThat(state.currentSpace).isNull()
assertThat(state.spaceInfo).isNotNull()
assertThat(state.children).isEmpty()
assertThat(state.seenSpaceInvites).isEmpty()
assertThat(state.hideInvitesAvatar).isFalse()
@ -139,23 +143,6 @@ class SpacePresenterTest {
}
}
@Test
fun `present - current space value`() = runTest {
val paginateResult = lambdaRecorder<Result<Unit>> {
Result.success(Unit)
}
val spaceRoomList = FakeSpaceRoomList(paginateResult = paginateResult)
val presenter = createSpacePresenter(spaceRoomList = spaceRoomList)
presenter.test {
val state = awaitItem()
advanceUntilIdle()
assertThat(state.currentSpace).isNull()
val aSpace = aSpaceRoom()
spaceRoomList.emitCurrentSpace(aSpace)
assertThat(awaitItem().currentSpace).isEqualTo(aSpace)
}
}
@Test
fun `present - children value`() = runTest {
val paginateResult = lambdaRecorder<Result<Unit>> {
@ -353,6 +340,216 @@ class SpacePresenterTest {
}
}
@Test
fun `present - enter manage mode`() = runTest {
val presenter = createSpacePresenter()
presenter.test {
val state = awaitItem()
assertThat(state.isManageMode).isFalse()
state.eventSink(SpaceEvents.EnterManageMode)
val manageModeState = awaitItem()
assertThat(manageModeState.isManageMode).isTrue()
assertThat(manageModeState.selectedRoomIds).isEmpty()
}
}
@Test
fun `present - exit manage mode clears selection`() = runTest {
val presenter = createSpacePresenter()
presenter.test {
val initialState = awaitItem()
initialState.eventSink(SpaceEvents.EnterManageMode)
initialState.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID))
initialState.eventSink(SpaceEvents.ExitManageMode)
val finalState = expectMostRecentItem()
assertThat(finalState.isManageMode).isFalse()
assertThat(finalState.selectedRoomIds).isEmpty()
}
}
@Test
fun `present - toggle room selection`() = runTest {
val presenter = createSpacePresenter()
presenter.test {
val initialState = awaitItem()
initialState.eventSink(SpaceEvents.EnterManageMode)
// Select a room
initialState.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID))
var latestState = expectMostRecentItem()
assertThat(latestState.selectedRoomIds).containsExactly(A_ROOM_ID)
// Deselect the room
latestState.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID))
latestState = expectMostRecentItem()
assertThat(latestState.selectedRoomIds).isEmpty()
}
}
@Test
fun `present - remove rooms success`() = runTest {
val removeChildFromSpaceResult = lambdaRecorder<RoomId, RoomId, Result<Unit>> { _, _ ->
Result.success(Unit)
}
val aRoom = aSpaceRoom(
roomId = A_ROOM_ID,
roomType = RoomType.Room,
)
val fakeSpaceRoomList = FakeSpaceRoomList(
initialSpaceRoomsValue = listOf(aRoom),
paginateResult = { Result.success(Unit) },
)
val presenter = createSpacePresenter(
spaceRoomList = fakeSpaceRoomList,
spaceService = FakeSpaceService(
removeChildFromSpaceResult = removeChildFromSpaceResult,
),
)
presenter.test {
awaitItem() // Initial empty state
advanceUntilIdle()
val stateWithChildren = awaitItem()
assertThat(stateWithChildren.children).hasSize(1)
stateWithChildren.eventSink(SpaceEvents.EnterManageMode)
stateWithChildren.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID))
stateWithChildren.eventSink(SpaceEvents.RemoveSelectedRooms)
stateWithChildren.eventSink(SpaceEvents.ConfirmRoomRemoval)
advanceUntilIdle()
val successState = expectMostRecentItem()
assertThat(successState.removeRoomsAction).isEqualTo(AsyncAction.Success(Unit))
assertThat(successState.isManageMode).isFalse()
assertThat(successState.children).isEmpty()
removeChildFromSpaceResult.assertions().isCalledOnce()
}
}
@Test
fun `present - remove rooms partial failure`() = runTest {
val aRoom1 = aSpaceRoom(
roomId = A_ROOM_ID,
roomType = RoomType.Room,
)
val aRoom2 = aSpaceRoom(
roomId = A_ROOM_ID_2,
roomType = RoomType.Room,
)
val removeChildFromSpaceResult = lambdaRecorder<RoomId, RoomId, Result<Unit>> { _, childId ->
if (childId == A_ROOM_ID_2) {
Result.failure(AN_EXCEPTION)
} else {
Result.success(Unit)
}
}
val fakeSpaceRoomList = FakeSpaceRoomList(
initialSpaceRoomsValue = listOf(aRoom1, aRoom2),
paginateResult = { Result.success(Unit) },
)
val presenter = createSpacePresenter(
spaceRoomList = fakeSpaceRoomList,
spaceService = FakeSpaceService(
removeChildFromSpaceResult = removeChildFromSpaceResult,
),
)
presenter.test {
awaitItem() // Initial empty state
advanceUntilIdle()
val stateWithChildren = awaitItem()
assertThat(stateWithChildren.children).hasSize(2)
stateWithChildren.eventSink(SpaceEvents.EnterManageMode)
stateWithChildren.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID))
stateWithChildren.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID_2))
stateWithChildren.eventSink(SpaceEvents.RemoveSelectedRooms)
stateWithChildren.eventSink(SpaceEvents.ConfirmRoomRemoval)
advanceUntilIdle()
val failureState = expectMostRecentItem()
assertThat(failureState.removeRoomsAction.isFailure()).isTrue()
// Successfully removed room should be filtered out
assertThat(failureState.children.map { it.roomId }).doesNotContain(A_ROOM_ID)
// Failed room should still be present
assertThat(failureState.children.map { it.roomId }).contains(A_ROOM_ID_2)
removeChildFromSpaceResult.assertions().isCalledExactly(2)
}
}
@Test
fun `present - children filtered in manage mode shows only rooms`() = runTest {
val aRoom = aSpaceRoom(
roomId = A_ROOM_ID,
roomType = RoomType.Room,
)
val aSubSpace = aSpaceRoom(
roomId = A_ROOM_ID_2,
roomType = RoomType.Space,
)
val fakeSpaceRoomList = FakeSpaceRoomList(
initialSpaceRoomsValue = listOf(aRoom, aSubSpace),
paginateResult = { Result.success(Unit) },
)
val presenter = createSpacePresenter(spaceRoomList = fakeSpaceRoomList)
presenter.test {
awaitItem() // Initial empty state
advanceUntilIdle()
val stateWithChildren = awaitItem()
// Both room and space visible initially
assertThat(stateWithChildren.children).hasSize(2)
assertThat(stateWithChildren.isManageMode).isFalse()
stateWithChildren.eventSink(SpaceEvents.EnterManageMode)
val manageModeState = expectMostRecentItem()
// Only rooms visible in manage mode
assertThat(manageModeState.children).hasSize(1)
assertThat(manageModeState.children.first().roomId).isEqualTo(A_ROOM_ID)
assertThat(manageModeState.children.first().isSpace).isFalse()
}
}
@Test
fun `present - removed rooms persist after flow update`() = runTest {
val removeChildFromSpaceResult = lambdaRecorder<RoomId, RoomId, Result<Unit>> { _, _ ->
Result.success(Unit)
}
val aRoom1 = aSpaceRoom(
roomId = A_ROOM_ID,
roomType = RoomType.Room,
)
val aRoom2 = aSpaceRoom(
roomId = A_ROOM_ID_2,
roomType = RoomType.Room,
)
val aRoom3 = aSpaceRoom(
roomId = A_ROOM_ID_3,
roomType = RoomType.Room,
)
val spaceRoomList = FakeSpaceRoomList(
initialSpaceRoomsValue = listOf(aRoom1, aRoom2),
paginateResult = { Result.success(Unit) },
)
val presenter = createSpacePresenter(
spaceRoomList = spaceRoomList,
spaceService = FakeSpaceService(
removeChildFromSpaceResult = removeChildFromSpaceResult,
),
)
presenter.test {
awaitItem() // Initial empty state
advanceUntilIdle()
val stateWithChildren = awaitItem()
stateWithChildren.eventSink(SpaceEvents.EnterManageMode)
stateWithChildren.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID))
stateWithChildren.eventSink(SpaceEvents.RemoveSelectedRooms)
stateWithChildren.eventSink(SpaceEvents.ConfirmRoomRemoval)
advanceUntilIdle()
val successState = expectMostRecentItem()
assertThat(successState.children.map { it.roomId }).doesNotContain(A_ROOM_ID)
// Emit new flow update with a new room added (simulating server refresh)
spaceRoomList.emitSpaceRooms(listOf(aRoom1, aRoom2, aRoom3))
advanceUntilIdle()
val afterFlowUpdate = awaitItem()
// A_ROOM_ID should still be filtered out even though it's in the new emission
assertThat(afterFlowUpdate.children.map { it.roomId }).doesNotContain(A_ROOM_ID)
// But the other rooms should be present
assertThat(afterFlowUpdate.children.map { it.roomId }).contains(A_ROOM_ID_2)
assertThat(afterFlowUpdate.children.map { it.roomId }).contains(A_ROOM_ID_3)
}
}
private fun TestScope.createSpacePresenter(
client: MatrixClient = FakeMatrixClient(),
room: BaseRoom = FakeBaseRoom(),
@ -365,6 +562,7 @@ class SpacePresenterTest {
),
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
spaceSettingsEnabled: Boolean = false,
spaceService: FakeSpaceService = FakeSpaceService(),
): SpacePresenter {
return SpacePresenter(
client = client,
@ -379,6 +577,7 @@ class SpacePresenterTest {
FeatureFlags.SpaceSettings.key to spaceSettingsEnabled,
)
),
spaceService = spaceService,
)
}
}

View file

@ -10,17 +10,19 @@ package io.element.android.features.space.impl.root
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.room.RoomType
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_ROOM_ID_3
import io.element.android.libraries.previewutils.room.aSpaceRoom
import org.junit.Test
class SpaceStateTest {
@Test
fun `test default state`() {
val state = aSpaceState()
assertThat(state.hasAnyFailure).isFalse()
assertThat(state.hasAnyJoinFailures).isFalse()
assertThat(state.isJoining(A_ROOM_ID)).isFalse()
}
@ -33,7 +35,7 @@ class SpaceStateTest {
A_ROOM_ID_3 to AsyncAction.Success(Unit),
)
)
assertThat(state.hasAnyFailure).isTrue()
assertThat(state.hasAnyJoinFailures).isTrue()
}
@Test
@ -45,4 +47,80 @@ class SpaceStateTest {
)
assertThat(state.isJoining(A_ROOM_ID)).isTrue()
}
@Test
fun `test isSelected returns true for selected room`() {
val state = aSpaceState(
selectedRoomIds = setOf(A_ROOM_ID)
)
assertThat(state.isSelected(A_ROOM_ID)).isTrue()
}
@Test
fun `test isSelected returns false for non-selected room`() {
val state = aSpaceState(
selectedRoomIds = setOf(A_ROOM_ID)
)
assertThat(state.isSelected(A_ROOM_ID_2)).isFalse()
}
@Test
fun `test showManageRoomsAction true when canManageRooms and has room children`() {
val state = aSpaceState(
canManageRooms = true,
children = listOf(aSpaceRoom(roomType = RoomType.Room))
)
assertThat(state.showManageRoomsAction).isTrue()
}
@Test
fun `test showManageRoomsAction false when canManageRooms but children empty`() {
val state = aSpaceState(
canManageRooms = true,
children = emptyList()
)
assertThat(state.showManageRoomsAction).isFalse()
}
@Test
fun `test showManageRoomsAction false when canManageRooms but only space children`() {
val state = aSpaceState(
canManageRooms = true,
children = listOf(aSpaceRoom(roomType = RoomType.Space))
)
assertThat(state.showManageRoomsAction).isFalse()
}
@Test
fun `test showManageRoomsAction false when has room children but canManageRooms false`() {
val state = aSpaceState(
canManageRooms = false,
children = listOf(aSpaceRoom(roomType = RoomType.Room))
)
assertThat(state.showManageRoomsAction).isFalse()
}
@Test
fun `test selectedCount returns correct count`() {
val state = aSpaceState(
selectedRoomIds = setOf(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3)
)
assertThat(state.selectedCount).isEqualTo(3)
}
@Test
fun `test isRemoveButtonEnabled true when selectedRoomIds not empty`() {
val state = aSpaceState(
selectedRoomIds = setOf(A_ROOM_ID)
)
assertThat(state.isRemoveButtonEnabled).isTrue()
}
@Test
fun `test isRemoveButtonEnabled false when selectedRoomIds empty`() {
val state = aSpaceState(
selectedRoomIds = emptySet()
)
assertThat(state.isRemoveButtonEnabled).isFalse()
}
}

View file

@ -15,11 +15,13 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_ROOM_TOPIC
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.libraries.previewutils.room.aSpaceRoom
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled
@ -29,6 +31,7 @@ import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.ensureCalledOnceWithParam
import io.element.android.tests.testutils.pressBack
import io.element.android.tests.testutils.pressBackKey
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
@ -124,7 +127,7 @@ class SpaceViewTest {
val eventsRecorder = EventsRecorder<SpaceEvents>()
rule.setSpaceView(
aSpaceState(
parentSpace = aSpaceRoom(topic = A_ROOM_TOPIC),
spaceInfo = aRoomInfo(topic = A_ROOM_TOPIC),
hasMoreToLoad = false,
eventSink = eventsRecorder,
)
@ -132,6 +135,71 @@ class SpaceViewTest {
rule.onNodeWithText(A_ROOM_TOPIC).performClick()
eventsRecorder.assertSingle(SpaceEvents.ShowTopicViewer(A_ROOM_TOPIC))
}
@Test
fun `clicking back in manage mode emits ExitManageMode event`() {
val eventsRecorder = EventsRecorder<SpaceEvents>()
rule.setSpaceView(
aSpaceState(
hasMoreToLoad = false,
isManageMode = true,
eventSink = eventsRecorder,
)
)
rule.pressBackKey()
eventsRecorder.assertSingle(SpaceEvents.ExitManageMode)
}
@Test
fun `clicking on room in manage mode emits ToggleRoomSelection event`() {
val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, displayName = A_ROOM_NAME)
val eventsRecorder = EventsRecorder<SpaceEvents>()
rule.setSpaceView(
aSpaceState(
children = listOf(aSpaceRoom),
hasMoreToLoad = false,
isManageMode = true,
eventSink = eventsRecorder,
)
)
rule.onNodeWithText(A_ROOM_NAME).performClick()
eventsRecorder.assertSingle(SpaceEvents.ToggleRoomSelection(A_ROOM_ID))
}
@Test
fun `clicking remove button emits RemoveSelectedRooms event`() {
val eventsRecorder = EventsRecorder<SpaceEvents>()
rule.setSpaceView(
aSpaceState(
children = listOf(aSpaceRoom(roomId = A_ROOM_ID)),
hasMoreToLoad = false,
isManageMode = true,
selectedRoomIds = setOf(A_ROOM_ID),
eventSink = eventsRecorder,
)
)
rule.clickOn(CommonStrings.action_remove)
eventsRecorder.assertSingle(SpaceEvents.RemoveSelectedRooms)
}
@Config(qualifiers = "h1024dp")
@Test
fun `clicking confirm in removal dialog emits ConfirmRoomRemoval event`() {
val eventsRecorder = EventsRecorder<SpaceEvents>()
rule.setSpaceView(
aSpaceState(
children = listOf(aSpaceRoom(roomId = A_ROOM_ID)),
hasMoreToLoad = false,
isManageMode = true,
selectedRoomIds = setOf(A_ROOM_ID),
removeRoomsAction = AsyncAction.ConfirmingNoParams,
eventSink = eventsRecorder,
)
)
// Click on the Remove button in the confirmation dialog
rule.clickOn(CommonStrings.action_remove, inDialog = true)
eventsRecorder.assertSingle(SpaceEvents.ConfirmRoomRemoval)
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSpaceView(

View file

@ -22,4 +22,12 @@ interface SpaceService {
fun spaceRoomList(id: RoomId): SpaceRoomList
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>
}

View file

@ -98,6 +98,12 @@ class RustSpaceService(
}
}
override suspend fun removeChildFromSpace(spaceId: RoomId, childId: RoomId): Result<Unit> = withContext(sessionDispatcher) {
runCatchingExceptions {
innerSpaceService.removeChildFromSpace(childId = childId.value, spaceId = spaceId.value)
}
}
init {
innerSpaceService
.spaceListUpdate()

View file

@ -23,6 +23,7 @@ class FakeSpaceService(
private val joinedSpacesResult: () -> Result<List<SpaceRoom>> = { lambdaError() },
private val spaceRoomListResult: (RoomId) -> SpaceRoomList = { lambdaError() },
private val leaveSpaceHandleResult: (RoomId) -> LeaveSpaceHandle = { lambdaError() },
private val removeChildFromSpaceResult: (RoomId, RoomId) -> Result<Unit> = { _, _ -> lambdaError() },
private val joinedParentsResult: (RoomId) -> Result<List<SpaceRoom>> = { lambdaError() },
private val getSpaceRoomResult: (RoomId) -> SpaceRoom? = { lambdaError() },
) : SpaceService {
@ -53,4 +54,8 @@ class FakeSpaceService(
override fun getLeaveSpaceHandle(spaceId: RoomId): LeaveSpaceHandle {
return leaveSpaceHandleResult(spaceId)
}
override suspend fun removeChildFromSpace(spaceId: RoomId, childId: RoomId): Result<Unit> = simulateLongTask {
removeChildFromSpaceResult(spaceId, childId)
}
}

View file

@ -333,7 +333,6 @@
<string name="screen_share_this_location_action">"Споделяне на това местоположение"</string>
<string name="screen_space_list_parent_space">"%1$s пространство"</string>
<string name="screen_space_list_title">"Пространства"</string>
<string name="screen_space_menu_action_members">"Преглед на членовете"</string>
<string name="screen_view_location_title">"Местоположение"</string>
<string name="settings_version_number">"Версия: %1$s (%2$s)"</string>
<string name="test_language_identifier">"bg"</string>

View file

@ -483,7 +483,6 @@ Opravdu chcete pokračovat?"</string>
<string name="screen_space_list_details">"%1$s • %2$s"</string>
<string name="screen_space_list_parent_space">"%1$s prostor"</string>
<string name="screen_space_list_title">"Prostory"</string>
<string name="screen_space_menu_action_members">"Zobrazit členy"</string>
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Zpráva nebyla odeslána, protože ověřená identita uživatele %1$s se změnila."</string>
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Zpráva nebyla odeslána, protože%1$s neověřil(a) všechna zařízení."</string>
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Zpráva nebyla odeslána, protože jste neověřili jedno nebo více zařízení."</string>

View file

@ -469,7 +469,6 @@ Er du sikker på, at du vil fortsætte?"</string>
<string name="screen_space_list_details">"%1$s•%2$s"</string>
<string name="screen_space_list_parent_space">"%1$s gruppe"</string>
<string name="screen_space_list_title">"Grupper"</string>
<string name="screen_space_menu_action_members">"Vis medlemmer"</string>
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Beskeden blev ikke sendt fordi %1$s s bekræftede identitet blev nulstillet."</string>
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Meddelelsen er ikke sendt, fordi %1$s ikke har bekræftet alle enheder."</string>
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Beskeden er ikke sendt, fordi du ikke har verificeret en eller flere af dine enheder."</string>

View file

@ -483,12 +483,6 @@ Möchtest du wirklich fortfahren?"</string>
<string name="screen_space_list_empty_state_title">"Erstelle einen Space, um Chats zu organisieren"</string>
<string name="screen_space_list_parent_space">"%1$s Space"</string>
<string name="screen_space_list_title">"Spaces"</string>
<string name="screen_space_menu_action_members">"Mitglieder anzeigen"</string>
<string name="screen_space_remove_rooms_confirmation_content">"Das Entfernen eines Chats hat keinen Einfluss auf die Beitrittsregeln. Um die Regeln zu ändern, gehe zu \"Raum Info\" und dann zu \"Datenschutz und Sicherheit\""</string>
<plurals name="screen_space_remove_rooms_confirmation_title">
<item quantity="one">"Chat aus %1$s entfernen"</item>
<item quantity="other">"%1$d chats aus %2$s entfernen"</item>
</plurals>
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Nachricht nicht gesendet, weil sich die verifizierte Identität von %1$s geändert hat."</string>
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Die Nachricht wurde nicht gesendet, weil %1$s nicht alle Geräte verifiziert hat."</string>
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Die Nachricht wurde nicht gesendet, weil du eines oder mehrere deiner Geräte nicht verifiziert hast."</string>

View file

@ -476,7 +476,6 @@ Kas sa oled kindel, et soovid jätkata?"</string>
<string name="screen_space_list_details">"%1$s • %2$s"</string>
<string name="screen_space_list_parent_space">"Kogukond: %1$s"</string>
<string name="screen_space_list_title">"Kogukonnad"</string>
<string name="screen_space_menu_action_members">"Vaata liikmeid"</string>
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Sõnum on saatmata, kuna kasutaja %1$s verifitseeritud identiteet on lähtestatud."</string>
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Sõnum on saatmata, kuna %1$s pole verifitseerinud kõiki oma seadmeid."</string>
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Kuna sa pole üks või enamgi oma seadet verifitseerinud, siis sinu sõnum on saatmata."</string>

View file

@ -400,7 +400,6 @@
<string name="screen_space_list_details">"%1$s • %2$s"</string>
<string name="screen_space_list_parent_space">"%1$s فضا"</string>
<string name="screen_space_list_title">"فضاها"</string>
<string name="screen_space_menu_action_members">"دیدن اعضا"</string>
<string name="screen_view_location_title">"مکان"</string>
<string name="settings_version_number">"نگارش : %1$s (%2$s)"</string>
<string name="test_language_identifier">"fa"</string>

View file

@ -470,7 +470,6 @@ Haluatko varmasti jatkaa?"</string>
<string name="screen_space_list_details">"%1$s • %2$s"</string>
<string name="screen_space_list_parent_space">"%1$s tila"</string>
<string name="screen_space_list_title">"Tilat"</string>
<string name="screen_space_menu_action_members">"Näytä jäsenet"</string>
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Viestiä ei lähetetty, koska käyttäjän %1$s vahvistettu identiteetti nollattiin."</string>
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Viestiä ei lähetetty, koska %1$s ei ole vahvistanut kaikkia laitteitaan."</string>
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Viestiä ei lähetetty, koska et ole vahvistanut yhtä tai useampaa laitettasi."</string>

View file

@ -483,7 +483,6 @@ Raison : %1$s."</string>
<string name="screen_space_list_empty_state_title">"Créer des espaces pour organiser les salons"</string>
<string name="screen_space_list_parent_space">"Espace %1$s"</string>
<string name="screen_space_list_title">"Espaces"</string>
<string name="screen_space_menu_action_members">"Voir les membres"</string>
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Le message na pas été envoyé car lidentité vérifiée de %1$s a été réinitialisée."</string>
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Le message na pas été envoyé car %1$s na pas vérifié tous ses appareils."</string>
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Message non envoyé car vous navez pas vérifié tous vos appareils."</string>

View file

@ -485,7 +485,6 @@ Jeste li sigurni da želite nastaviti?"</string>
<string name="screen_space_list_details">"%1$s • %2$s"</string>
<string name="screen_space_list_parent_space">"Prostor %1$s"</string>
<string name="screen_space_list_title">"Prostori"</string>
<string name="screen_space_menu_action_members">"Prikaži članove"</string>
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Poruka nije poslana jer je poništen potvrđeni identitet korisnika %1$s."</string>
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Poruka nije poslana jer %1$s nije potvrdio sve uređaje."</string>
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Poruka nije poslana jer niste potvrdili jedan svoj uređaj ili više njih."</string>

View file

@ -469,7 +469,6 @@ Biztos, hogy folytatja?"</string>
<string name="screen_space_list_details">"%1$s • %2$s"</string>
<string name="screen_space_list_parent_space">"%1$s tér"</string>
<string name="screen_space_list_title">"Terek"</string>
<string name="screen_space_menu_action_members">"Tagok megtekintése"</string>
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Az üzenet nem lett elküldve, mert %1$s ellenőrzött személyazonossága megváltozott."</string>
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Az üzenet nem lett elküldve, mert %1$s nem ellenőrizte az összes eszközét."</string>
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Az üzenet nem lett elküldve, mert egy vagy több eszközét nem ellenőrizte."</string>

View file

@ -470,7 +470,6 @@ Sei sicuro di voler continuare?"</string>
<string name="screen_space_list_details">"%1$s • %2$s"</string>
<string name="screen_space_list_parent_space">"%1$s spazio"</string>
<string name="screen_space_list_title">"Spazi"</string>
<string name="screen_space_menu_action_members">"Visualizza membri"</string>
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Messaggio non inviato perché l\'identità verificata di %1$s è stata reimpostata."</string>
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Messaggio non inviato perché %1$s non ha verificato tutti i dispositivi."</string>
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Messaggio non inviato perché non hai verificato uno o più dispositivi."</string>

View file

@ -467,7 +467,6 @@ Er du sikker på at du vil fortsette?"</string>
<string name="screen_space_list_details">"%1$s • %2$s"</string>
<string name="screen_space_list_parent_space">"%1$s område"</string>
<string name="screen_space_list_title">"Områder"</string>
<string name="screen_space_menu_action_members">"Vis medlemmer"</string>
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Meldingen ble ikke sendt fordi %1$ss verifiserte identitet er tilbakestilt."</string>
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Meldingen ble ikke sendt fordi %1$s ikke har verifisert alle enheter."</string>
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Meldingen ble ikke sendt fordi du ikke har verifisert en eller flere av enhetene dine."</string>

View file

@ -479,7 +479,6 @@ Você tem certeza de que deseja continuar?"</string>
<string name="screen_space_list_details">"%1$s • %2$s"</string>
<string name="screen_space_list_parent_space">"Espaço %1$s"</string>
<string name="screen_space_list_title">"Espaços"</string>
<string name="screen_space_menu_action_members">"Ver membros"</string>
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Mensagem não enviada porque a identidade verificada de %1$s foi redefinida."</string>
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"A mensagem não foi enviada porque %1$s não verificou todos os dispositivos."</string>
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Mensagem não enviada porque você não verificou um ou mais dos seus dispositivos."</string>

View file

@ -484,7 +484,6 @@ Sunteți sigur că doriți să continuați?"</string>
<string name="screen_space_list_details">"%1$s • %2$s"</string>
<string name="screen_space_list_parent_space">"Spațiu %1$s"</string>
<string name="screen_space_list_title">"Spații"</string>
<string name="screen_space_menu_action_members">"Vizualizați membrii"</string>
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Mesajul nu a fost trimis deoarece identitatea verificată a lui %1$s s-a schimbat."</string>
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Mesajul nu a fost trimis deoarece %1$s nu a verificat toate dispozitivele."</string>
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Mesajul nu a fost trimis deoarece nu ați verificat unul sau mai multe dispozitive."</string>

View file

@ -479,7 +479,6 @@
<string name="screen_space_list_details">"%1$s • %2$s"</string>
<string name="screen_space_list_parent_space">"%1$s пространство"</string>
<string name="screen_space_list_title">"Пространства"</string>
<string name="screen_space_menu_action_members">"Просмотреть участников"</string>
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Сообщение не отправлено, потому что подтвержденная личность %1$s была сброшена."</string>
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Сообщение не отправлено, потому что %1$s не проверил одно или несколько устройств."</string>
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Сообщение не отправлено, поскольку вы не подтвердили одно или несколько своих устройств."</string>

View file

@ -481,7 +481,6 @@ Naozaj chcete pokračovať?"</string>
<string name="screen_space_list_details">"%1$s • %2$s"</string>
<string name="screen_space_list_parent_space">"%1$s priestor"</string>
<string name="screen_space_list_title">"Priestory"</string>
<string name="screen_space_menu_action_members">"Zobraziť členov"</string>
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Správa nebola odoslaná, pretože sa zmenila overená totožnosť používateľa %1$s."</string>
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Správa nebola odoslaná, pretože %1$s neoveril/a všetky zariadenia."</string>
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Správa nebola odoslaná, pretože ste neoverili jedno alebo viac svojich zariadení."</string>

View file

@ -461,7 +461,6 @@
<string name="screen_space_list_details">"%1$s • %2$s"</string>
<string name="screen_space_list_parent_space">"%1$s 空間"</string>
<string name="screen_space_list_title">"空間"</string>
<string name="screen_space_menu_action_members">"檢視成員"</string>
<string name="screen_timeline_item_menu_send_failure_changed_identity">"因為 %1$s 的驗證身份已重設,因此未傳送訊息。"</string>
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"訊息未傳送,因為 %1$s 尚未驗證所有裝置。"</string>
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"因為您尚未驗證一個或多個裝置,因此未傳送訊息"</string>

View file

@ -460,7 +460,6 @@
<string name="screen_space_list_details">"%1$s • %2$s"</string>
<string name="screen_space_list_parent_space">"%1$s空间"</string>
<string name="screen_space_list_title">"空间"</string>
<string name="screen_space_menu_action_members">"查看成员"</string>
<string name="screen_timeline_item_menu_send_failure_changed_identity">"消息未发送,因为%1$s的已验证身份已被重置。"</string>
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"消息未发送,因为%1$s尚未验证所有设备。"</string>
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"消息未发送,因为您有尚未验证的设备。"</string>

View file

@ -317,6 +317,10 @@ Reason: %1$s."</string>
<string name="common_security">"Security"</string>
<string name="common_seen_by">"Seen by"</string>
<string name="common_select_account">"Select an account"</string>
<plurals name="common_selected_count">
<item quantity="one">"%1$d selected"</item>
<item quantity="other">"%1$d selected"</item>
</plurals>
<string name="common_send_to">"Send to"</string>
<string name="common_sending">"Sending…"</string>
<string name="common_sending_failed">"Sending failed"</string>
@ -484,12 +488,6 @@ Are you sure you want to continue?"</string>
<string name="screen_space_list_empty_state_title">"Create spaces to organize rooms"</string>
<string name="screen_space_list_parent_space">"%1$s space"</string>
<string name="screen_space_list_title">"Spaces"</string>
<string name="screen_space_menu_action_members">"View members"</string>
<string name="screen_space_remove_rooms_confirmation_content">"Removing a room will not affect the room access. To change the access go to Room info &gt; Privacy &amp; security."</string>
<plurals name="screen_space_remove_rooms_confirmation_title">
<item quantity="one">"Remove room from %1$s"</item>
<item quantity="other">"Remove %1$d rooms from %2$s"</item>
</plurals>
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Message not sent because %1$ss verified identity was reset."</string>
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Message not sent because %1$s has not verified all devices."</string>
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Message not sent because you have not verified one or more of your devices."</string>

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c55277089a4447b618a0e8c058718ecf9d3da6d437322f0e23e5fd70019f6b00
size 34585
oid sha256:e59a9e2ae6ef36f28e61534b5639314cc840953df51bb1660e77e8d565865357
size 32998

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:241f5500cb7212fac174466bbe7855ccf39de3e3764a83202388b947d90ae807
size 34770
oid sha256:b854ce2b0618ebcbc88eff9952d6869bceeb8b43f9eccb8f8e5feef225e0a4c2
size 33181

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:92785cf3a4010779b0fbcd58be3437a22808b0a2f02a19a5cfd50eb3bd58ed26
size 35058
oid sha256:e1cf063ee5c9fbc50a53445050780ba239d4fb0fd1e9903a578eab8dd3bfc257
size 33496

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:de26882f13bac98b2cb5365d98e06e781d516d179adb8328cf22cf524e6fd79e
size 62568
oid sha256:669287557e8effe8154682d33de45430fc852ddefa25ed7399aa3917730e2893
size 61083

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a121fdb9473512b0264e48294df1799a7a6bf9b469df973fbe41f31bbf98f1d0
size 63248
oid sha256:d2ef07a1af8f872a5b7bb708314bc180d4eefe9545c763a0860f895af6e3ce37
size 61755

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a3506b4f646408262450ae51b612f86c1171ed972c1d7ea8871c4dc090556c7a
size 59702
oid sha256:525059397001897f705630b8ac5a661439a502f2e623fdca252f9e86f97133e4
size 58181

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:59af097a2152b235c7df83aa76eea880ceef65edebebecb86f00745ec39712f4
size 35937

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a5151bbd3effcd6a46724a0cfb13730e845a3b8ba489826146c76d0a797403e0
size 36502

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2b445ae9e0ff01dea22b8ffb98aaaf4c5593093e2b2bea2ff9508cc43832e687
size 48283

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f2407444889af236ef21a90c47a5d3e05df8b15b9cc9483e84377e3af8794772
size 33996
oid sha256:47c8bdf4d153ecebe749ae3cacfe4c9a1c59ac836106fb0903dca80305aedac2
size 32412

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fee41efefc2ca1d6670d8455ac756c6b314aab54510eab8a4e597f1cc1edf3f8
size 34141
oid sha256:33917297cb1c6d38e5b955757de50f4ff6b73844bb87e8e88d0885daa01b266b
size 32556

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ea955839cbd1aeba5de2780cee413628c7d46383398b10125cd3a900fb41d5a5
size 34459
oid sha256:930f763051533ea3aa4032e483e15e0c1ccb1d213d2e59bebe7f934517b500ba
size 32868

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cba2c99744aeb2a869ae2ed700d7241b1d0b6ed979b16d2be9774ddbc5f8f28a
size 61381
oid sha256:e38c4a6ff464f77da95b2aa0eaba647bc54ee590c2c4319478fcd54a2400b886
size 59877

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b32d65accabc357208efeb2ec61374182479541299ade28184f82938e59bfdd5
size 61932
oid sha256:8d91c27ee6dc7938ce905774868b66157b06944ae7c230935c30d6be8ce189be
size 60430

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7c161ff55e8a235fe403e53ee179b299fd2563d85ef64bfe6d0dd9295228685b
size 57925
oid sha256:dd673c2cf628285836848a732d3972a17421a2394464a3a77e7a49e4c5a862f1
size 56636

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b459fa8aa195dcf85995ac51c2f079e9d2505efede138bcef94c0a5049e1f271
size 35266

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:36fd123fcff07169723857c99e4e375fe9bbb8193b2a6e97508343ea025d5787
size 35782

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a5eed7b1690ac5fbd24312e37029ca880b5cf4424e3796b116829074ef891f2d
size 45433

View file

@ -233,7 +233,8 @@
"name" : ":features:space:impl",
"includeRegex" : [
"screen\\.leave_space\\..*",
"screen\\.space_settings\\..*"
"screen\\.space_settings\\..*",
"screen\\.space\\..*"
]
},
{