diff --git a/changelog.d/2257.feature b/changelog.d/2257.feature new file mode 100644 index 0000000000..3e632d8bb3 --- /dev/null +++ b/changelog.d/2257.feature @@ -0,0 +1 @@ +Admins can now change user roles in rooms. diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleUserListStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleUserListStateProvider.kt index 9c7991de6b..4fb1149a01 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleUserListStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleUserListStateProvider.kt @@ -19,7 +19,6 @@ package io.element.android.features.createroom.impl.addpeople import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.createroom.impl.userlist.SelectionMode import io.element.android.features.createroom.impl.userlist.UserListState -import io.element.android.features.createroom.impl.userlist.aListOfSelectedUsers import io.element.android.features.createroom.impl.userlist.aUserListState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.ui.components.aMatrixUserList @@ -32,7 +31,7 @@ open class AddPeopleUserListStateProvider : PreviewParameterProvider if (checked) { onUserSelected(searchResult.matrixUser) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/UserListView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/UserListView.kt index b13d682250..3cc009989a 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/UserListView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/UserListView.kt @@ -29,7 +29,7 @@ import io.element.android.features.createroom.impl.userlist.UserListStateProvide import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.matrix.api.user.MatrixUser -import io.element.android.libraries.matrix.ui.components.SelectedUsersList +import io.element.android.libraries.matrix.ui.components.SelectedUsersRowList @Composable fun UserListView( @@ -64,7 +64,7 @@ fun UserListView( ) if (state.isMultiSelectionEnabled && !state.isSearchActive && state.selectedUsers.isNotEmpty()) { - SelectedUsersList( + SelectedUsersRowList( contentPadding = PaddingValues(16.dp), selectedUsers = state.selectedUsers, autoScroll = true, diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt index 1065f746e0..7eceffd7c4 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt @@ -18,10 +18,11 @@ package io.element.android.features.createroom.impl.configureroom import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.createroom.impl.CreateRoomConfig -import io.element.android.features.createroom.impl.userlist.aListOfSelectedUsers import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.ui.components.aMatrixUserList import io.element.android.libraries.permissions.api.aPermissionsState import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList open class ConfigureRoomStateProvider : PreviewParameterProvider { override val values: Sequence @@ -31,7 +32,7 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider> = MutableStateFlow(emptyList()) fun selectUser(user: MatrixUser) { - if (user !in selectedUsers.value) { + if (!selectedUsers.value.contains(user)) { selectedUsers.tryEmit(selectedUsers.value.plus(user)) } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListStateProvider.kt index 4685bb47e3..225812b415 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListStateProvider.kt @@ -19,7 +19,6 @@ package io.element.android.features.createroom.impl.userlist import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.ui.components.aMatrixUserList -import io.element.android.libraries.usersearch.api.UserSearchResult import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -38,15 +37,15 @@ open class UserListStateProvider : PreviewParameterProvider { aUserListState().copy( isSearchActive = true, searchQuery = "@someone:matrix.org", - selectedUsers = aListOfSelectedUsers(), - searchResults = SearchBarResultState.Results(aMatrixUserList().map { UserSearchResult(it) }.toImmutableList()), + selectedUsers = aMatrixUserList().toImmutableList(), + searchResults = SearchBarResultState.Results(aListOfSelectedUsers()), ), aUserListState().copy( isSearchActive = true, searchQuery = "@someone:matrix.org", selectionMode = SelectionMode.Multiple, - selectedUsers = aListOfSelectedUsers(), - searchResults = SearchBarResultState.Results(aMatrixUserList().map { UserSearchResult(it) }.toImmutableList()), + selectedUsers = aMatrixUserList().toImmutableList(), + searchResults = SearchBarResultState.Results(aListOfSelectedUsers()), ), aUserListState().copy( isSearchActive = true, diff --git a/features/messages/impl/src/main/res/values/localazy.xml b/features/messages/impl/src/main/res/values/localazy.xml index 3223cd6036..e3116edffc 100644 --- a/features/messages/impl/src/main/res/values/localazy.xml +++ b/features/messages/impl/src/main/res/values/localazy.xml @@ -48,6 +48,8 @@ "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges." "Demote yourself?" "Edit Moderators" + "You have unsaved changes." + "Save changes?" "Message history is currently unavailable." "Message history is unavailable in this room. Verify this device to see your message history." "Failed processing media to upload, please try again." @@ -77,6 +79,9 @@ "Send again" "Your message failed to send" "Admins" + "Change my role" + "Demote to member" + "Demote to moderator" "Member moderation" "Messages and content" "Moderators" diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt index 959d209d88..db41748747 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt @@ -36,6 +36,7 @@ import io.element.android.features.roomdetails.impl.members.RoomMemberListNode import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsNode import io.element.android.features.roomdetails.impl.members.details.avatar.AvatarPreviewNode import io.element.android.features.roomdetails.impl.notificationsettings.RoomNotificationSettingsNode +import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsFlowNode import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.createNode @@ -91,6 +92,9 @@ class RoomDetailsFlowNode @AssistedInject constructor( @Parcelize data object PollHistory : NavTarget + + @Parcelize + data object AdminSettings : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -120,6 +124,10 @@ class RoomDetailsFlowNode @AssistedInject constructor( override fun openPollHistory() { backstack.push(NavTarget.PollHistory) } + + override fun openAdminSettings() { + backstack.push(NavTarget.AdminSettings) + } } createNode(buildContext, listOf(roomDetailsCallback)) } @@ -189,6 +197,10 @@ class RoomDetailsFlowNode @AssistedInject constructor( is NavTarget.PollHistory -> { pollHistoryEntryPoint.createNode(this, buildContext) } + + is NavTarget.AdminSettings -> { + createNode(buildContext) + } } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt index d57a067a95..63cc745ecc 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt @@ -53,6 +53,7 @@ class RoomDetailsNode @AssistedInject constructor( fun openRoomNotificationSettings() fun openAvatarPreview(name: String, url: String) fun openPollHistory() + fun openAdminSettings() } private val callbacks = plugins() @@ -119,6 +120,10 @@ class RoomDetailsNode @AssistedInject constructor( callbacks.forEach { it.openAvatarPreview(name, url) } } + private fun openAdminSettings() { + callbacks.forEach { it.openAdminSettings() } + } + @Composable override fun View(modifier: Modifier) { val context = LocalContext.current @@ -151,6 +156,7 @@ class RoomDetailsNode @AssistedInject constructor( invitePeople = ::invitePeople, openAvatarPreview = ::openAvatarPreview, openPollHistory = ::openPollHistory, + openAdminSettings = this::openAdminSettings, ) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt index fdbdc77910..6fef4a59bb 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt @@ -47,6 +47,7 @@ import io.element.android.libraries.matrix.api.room.powerlevels.canInvite import io.element.android.libraries.matrix.api.room.powerlevels.canSendState import io.element.android.libraries.matrix.api.room.roomNotificationSettings import io.element.android.libraries.matrix.ui.room.getDirectRoomMember +import io.element.android.libraries.matrix.ui.room.isOwnUserAdmin import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analyticsproviders.api.trackers.captureInteraction import kotlinx.coroutines.CoroutineScope @@ -71,6 +72,7 @@ class RoomDetailsPresenter @Inject constructor( val leaveRoomState = leaveRoomPresenter.present() val canShowNotificationSettings = remember { mutableStateOf(false) } val roomInfo by room.roomInfoFlow.collectAsState(initial = null) + val isUserAdmin = room.isOwnUserAdmin() val roomAvatar by remember { derivedStateOf { roomInfo?.avatarUrl ?: room.avatarUrl } } @@ -150,6 +152,7 @@ class RoomDetailsPresenter @Inject constructor( leaveRoomState = leaveRoomState, roomNotificationSettings = roomNotificationSettingsState.roomNotificationSettings(), isFavorite = isFavorite, + displayAdminSettings = !room.isDm && isUserAdmin, eventSink = ::handleEvents, ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt index 5ba39af5bf..95f90e2c70 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt @@ -37,6 +37,7 @@ data class RoomDetailsState( val leaveRoomState: LeaveRoomState, val roomNotificationSettings: RoomNotificationSettings?, val isFavorite: Boolean, + val displayAdminSettings: Boolean, val eventSink: (RoomDetailsEvent) -> Unit ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt index 1649b15dd2..5cd57b42c9 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt @@ -30,7 +30,7 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationSettings open class RoomDetailsStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - aRoomDetailsState(), + aRoomDetailsState(displayAdminSettings = true), aRoomDetailsState(roomTopic = RoomTopicState.Hidden), aRoomDetailsState(roomTopic = RoomTopicState.CanAddTopic), aRoomDetailsState(isEncrypted = false), @@ -92,6 +92,7 @@ fun aRoomDetailsState( leaveRoomState: LeaveRoomState = aLeaveRoomState(), roomNotificationSettings: RoomNotificationSettings = aRoomNotificationSettings(), isFavorite: Boolean = false, + displayAdminSettings: Boolean = false, eventSink: (RoomDetailsEvent) -> Unit = {}, ) = RoomDetailsState( roomId = roomId, @@ -109,6 +110,7 @@ fun aRoomDetailsState( leaveRoomState = leaveRoomState, roomNotificationSettings = roomNotificationSettings, isFavorite = isFavorite, + displayAdminSettings = displayAdminSettings, eventSink = eventSink ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt index da85f5d980..d1f2f82fce 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt @@ -97,6 +97,7 @@ fun RoomDetailsView( invitePeople: () -> Unit, openAvatarPreview: (name: String, url: String) -> Unit, openPollHistory: () -> Unit, + openAdminSettings: () -> Unit, modifier: Modifier = Modifier, ) { fun onShareMember() { @@ -160,30 +161,45 @@ fun RoomDetailsView( ) } - if (state.canShowNotificationSettings && state.roomNotificationSettings != null) { - NotificationSection( - isDefaultMode = state.roomNotificationSettings.isDefault, - openRoomNotificationSettings = openRoomNotificationSettings + PreferenceCategory { + if (state.canShowNotificationSettings && state.roomNotificationSettings != null) { + NotificationItem( + isDefaultMode = state.roomNotificationSettings.isDefault, + openRoomNotificationSettings = openRoomNotificationSettings + ) + } + + FavoriteItem( + isFavorite = state.isFavorite, + onFavoriteChanges = { + state.eventSink(RoomDetailsEvent.SetFavorite(it)) + } ) + + if (state.displayAdminSettings) { + ListItem( + headlineContent = { Text("Roles and permissions") }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Admin())), + onClick = openAdminSettings, + ) + } } - FavoriteSection( - isFavorite = state.isFavorite, - onFavoriteChanges = { - state.eventSink(RoomDetailsEvent.SetFavorite(it)) - } - ) - - if (state.roomType is RoomDetailsType.Room) { - MembersSection( - memberCount = state.memberCount, - openRoomMemberList = openRoomMemberList, - ) - - if (state.canInvite) { - InviteSection( - invitePeople = invitePeople - ) + val displayMemberListItem = state.roomType is RoomDetailsType.Room + val displayInviteMembersItem = state.canInvite + if (displayMemberListItem || displayInviteMembersItem) { + PreferenceCategory { + if (displayMemberListItem) { + MembersItem( + memberCount = state.memberCount, + openRoomMemberList = openRoomMemberList, + ) + } + if (displayInviteMembersItem) { + InviteItem( + invitePeople = invitePeople + ) + } } } @@ -349,7 +365,7 @@ private fun TopicSection( } @Composable -private fun NotificationSection( +private fun NotificationItem( isDefaultMode: Boolean, openRoomNotificationSettings: () -> Unit, ) { @@ -358,58 +374,49 @@ private fun NotificationSection( } else { stringResource(R.string.screen_room_details_notification_mode_custom) } - PreferenceCategory { - ListItem( - headlineContent = { Text(text = stringResource(R.string.screen_room_details_notification_title)) }, - supportingContent = { Text(text = subtitle) }, - leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Notifications())), - onClick = openRoomNotificationSettings, - ) - } + ListItem( + headlineContent = { Text(text = stringResource(R.string.screen_room_details_notification_title)) }, + supportingContent = { Text(text = subtitle) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Notifications())), + onClick = openRoomNotificationSettings, + ) } @Composable -private fun FavoriteSection( +private fun FavoriteItem( isFavorite: Boolean, onFavoriteChanges: (Boolean) -> Unit, - modifier: Modifier = Modifier ) { - PreferenceCategory(modifier = modifier) { - PreferenceSwitch( - icon = CompoundIcons.Favourite(), - title = stringResource(id = CommonStrings.common_favourite), - isChecked = isFavorite, - onCheckedChange = onFavoriteChanges - ) - } + PreferenceSwitch( + icon = CompoundIcons.Favourite(), + title = stringResource(id = CommonStrings.common_favourite), + isChecked = isFavorite, + onCheckedChange = onFavoriteChanges + ) } @Composable -private fun MembersSection( +private fun MembersItem( memberCount: Long, openRoomMemberList: () -> Unit, ) { - PreferenceCategory { - ListItem( - headlineContent = { Text(stringResource(CommonStrings.common_people)) }, - leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.User())), - trailingContent = ListItemContent.Text(memberCount.toString()), - onClick = openRoomMemberList, - ) - } + ListItem( + headlineContent = { Text(stringResource(CommonStrings.common_people)) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.User())), + trailingContent = ListItemContent.Text(memberCount.toString()), + onClick = openRoomMemberList, + ) } @Composable -private fun InviteSection( +private fun InviteItem( invitePeople: () -> Unit, ) { - PreferenceCategory { - ListItem( - headlineContent = { Text(stringResource(R.string.screen_room_details_invite_people_title)) }, - leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.UserAdd())), - onClick = invitePeople, - ) - } + ListItem( + headlineContent = { Text(stringResource(R.string.screen_room_details_invite_people_title)) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.UserAdd())), + onClick = invitePeople, + ) } @Composable @@ -481,5 +488,6 @@ private fun ContentToPreview(state: RoomDetailsState) { invitePeople = {}, openAvatarPreview = { _, _ -> }, openPollHistory = {}, + openAdminSettings = {}, ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenter.kt index 971db90450..60f5f82ebd 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenter.kt @@ -54,7 +54,7 @@ class RoomInviteMembersPresenter @Inject constructor( val searchResults = remember { mutableStateOf>>(SearchBarResultState.Initial()) } var searchQuery by rememberSaveable { mutableStateOf("") } var searchActive by rememberSaveable { mutableStateOf(false) } - var showSearchLoader = rememberSaveable { mutableStateOf(false) } + val showSearchLoader = rememberSaveable { mutableStateOf(false) } LaunchedEffect(Unit) { fetchMembers(roomMembers) @@ -99,7 +99,7 @@ class RoomInviteMembersPresenter @Inject constructor( @JvmName("toggleUserInSelectedUsers") private fun MutableState>.toggleUser(user: MatrixUser) { value = if (value.contains(user)) { - value.filterNot { it == user } + value.filterNot { it.userId == user.userId } } else { value + user }.toImmutableList() diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt index bc94519b3e..39c2118580 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt @@ -48,7 +48,7 @@ import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.components.CheckableUserRow import io.element.android.libraries.matrix.ui.components.CheckableUserRowData -import io.element.android.libraries.matrix.ui.components.SelectedUsersList +import io.element.android.libraries.matrix.ui.components.SelectedUsersRowList import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.matrix.ui.model.getBestName import io.element.android.libraries.ui.strings.CommonStrings @@ -97,7 +97,7 @@ fun RoomInviteMembersView( ) if (!state.isSearchActive) { - SelectedUsersList( + SelectedUsersRowList( modifier = Modifier.fillMaxWidth(), selectedUsers = state.selectedUsers, autoScroll = true, @@ -157,7 +157,7 @@ private fun RoomInviteMembersSearchBar( placeHolderTitle = placeHolderTitle, contentPrefix = { if (selectedUsers.isNotEmpty()) { - SelectedUsersList( + SelectedUsersRowList( modifier = Modifier.fillMaxWidth(), selectedUsers = selectedUsers, autoScroll = true, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsEvents.kt new file mode 100644 index 0000000000..249d934816 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsEvents.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.rolesandpermissions + +import io.element.android.libraries.matrix.api.room.RoomMember + +sealed interface RolesAndPermissionsEvents { + data object ChangeOwnRole : RolesAndPermissionsEvents + data class DemoteSelfTo(val role: RoomMember.Role) : RolesAndPermissionsEvents + data object CancelPendingAction : RolesAndPermissionsEvents +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsFlowNode.kt new file mode 100644 index 0000000000..6c38551441 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsFlowNode.kt @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.rolesandpermissions + +import android.os.Parcelable +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.push +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles.ChangeRolesNode +import io.element.android.libraries.architecture.BackstackView +import io.element.android.libraries.architecture.BaseFlowNode +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.RoomScope +import kotlinx.parcelize.Parcelize + +@ContributesNode(RoomScope::class) +class RolesAndPermissionsFlowNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, +) : BaseFlowNode( + backstack = BackStack( + initialElement = NavTarget.AdminSettings, + savedStateMap = buildContext.savedStateMap, + ), + buildContext = buildContext, + plugins = plugins, +) { + sealed interface NavTarget : Parcelable { + @Parcelize + data object AdminSettings : NavTarget + + @Parcelize + data object AdminList : NavTarget + + @Parcelize + data object ModeratorList : NavTarget + } + + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { + return when (navTarget) { + is NavTarget.AdminSettings -> { + val callback = object : RolesAndPermissionsNode.Callback { + override fun openAdminList() { + backstack.push(NavTarget.AdminList) + } + + override fun openModeratorList() { + backstack.push(NavTarget.ModeratorList) + } + } + createNode( + buildContext = buildContext, + plugins = listOf(callback), + ) + } + is NavTarget.AdminList -> { + val inputs = ChangeRolesNode.Inputs(ChangeRolesNode.ListType.Admins) + createNode( + buildContext = buildContext, + plugins = listOf(inputs), + ) + } + is NavTarget.ModeratorList -> { + val inputs = ChangeRolesNode.Inputs(ChangeRolesNode.ListType.Moderators) + createNode( + buildContext = buildContext, + plugins = listOf(inputs), + ) + } + } + } + + @Composable + override fun View(modifier: Modifier) { + BackstackView() + } +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsNode.kt new file mode 100644 index 0000000000..1e78302146 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsNode.kt @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.rolesandpermissions + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.roomMembers +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.launch + +@ContributesNode(RoomScope::class) +class RolesAndPermissionsNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: RolesAndPermissionsPresenter, + private val room: MatrixRoom, +) : Node(buildContext, plugins = plugins), RoomDetailsAdminSettingsNavigator { + interface Callback : Plugin { + fun openAdminList() + fun openModeratorList() + } + + private val callback = plugins().first() + + override fun onBackPressed() = navigateUp() + override fun openAdminList() = callback.openAdminList() + override fun openModeratorList() = callback.openModeratorList() + + override fun onBuilt() { + super.onBuilt() + + // Reload members when the user sees this screen + lifecycle.addObserver(object : LifecycleEventObserver { + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + if (event == Lifecycle.Event.ON_RESUME) { + lifecycleScope.launch { room.updateMembers() } + } + } + }) + + // If the user is not an admin anymore, exit this section since they won't have permissions to use it + lifecycleScope.launch { + room.membersStateFlow + .map { state -> + state.roomMembers().orEmpty().find { it.userId == room.sessionId } + } + .filter { it?.role != RoomMember.Role.ADMIN } + .take(1) + .onEach { navigateUp() } + .collect() + } + } + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + RolesAndPermissionsView( + state = state, + roomDetailsAdminSettingsNavigator = this, + modifier = modifier, + ) + } +} + +interface RoomDetailsAdminSettingsNavigator { + fun onBackPressed() {} + fun openAdminList() {} + fun openModeratorList() {} +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsPresenter.kt new file mode 100644 index 0000000000..db67de66a4 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsPresenter.kt @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.rolesandpermissions + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.runUpdatingState +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.MatrixRoomInfo +import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import javax.inject.Inject + +class RolesAndPermissionsPresenter @Inject constructor( + private val room: MatrixRoom, + private val dispatchers: CoroutineDispatchers, +) : Presenter { + @Composable + override fun present(): RolesAndPermissionsState { + val coroutineScope = rememberCoroutineScope() + val roomInfo by room.roomInfoFlow.collectAsState(initial = null) + val moderatorCount by remember { + derivedStateOf { + roomInfo.userCountWithRole(RoomMember.Role.MODERATOR) + } + } + val adminCount by remember { + derivedStateOf { + roomInfo.userCountWithRole(RoomMember.Role.ADMIN) + } + } + val changeOwnRoleAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } + + fun handleEvent(event: RolesAndPermissionsEvents) { + when (event) { + is RolesAndPermissionsEvents.ChangeOwnRole -> { + changeOwnRoleAction.value = AsyncAction.Confirming + } + is RolesAndPermissionsEvents.CancelPendingAction -> { + changeOwnRoleAction.value = AsyncAction.Uninitialized + } + is RolesAndPermissionsEvents.DemoteSelfTo -> coroutineScope.demoteSelfTo( + role = event.role, + changeOwnRoleAction = changeOwnRoleAction, + ) + } + } + + return RolesAndPermissionsState( + adminCount = adminCount, + moderatorCount = moderatorCount, + changeOwnRoleAction = changeOwnRoleAction.value, + eventSink = { handleEvent(it) }, + ) + } + + private fun CoroutineScope.demoteSelfTo( + role: RoomMember.Role, + changeOwnRoleAction: MutableState>, + ) = launch(dispatchers.io) { + runUpdatingState(changeOwnRoleAction) { + room.updateUsersRoles(listOf(UserRoleChange(room.sessionId, role))) + } + } + + private fun MatrixRoomInfo?.userCountWithRole(role: RoomMember.Role): Int { + return if (this != null) { + userPowerLevels.count { (_, level) -> RoomMember.Role.forPowerLevel(level) == role } + } else { + 0 + } + } +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsState.kt new file mode 100644 index 0000000000..b1c2905ae8 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsState.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.rolesandpermissions + +import io.element.android.libraries.architecture.AsyncAction + +data class RolesAndPermissionsState( + val adminCount: Int, + val moderatorCount: Int, + val changeOwnRoleAction: AsyncAction, + val eventSink: (RolesAndPermissionsEvents) -> Unit, +) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsStateProvider.kt new file mode 100644 index 0000000000..cde21c1e7e --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsStateProvider.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.rolesandpermissions + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.AsyncAction + +class RolesAndPermissionsStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aRolesAndPermissionsState(), + aRolesAndPermissionsState(adminCount = 1, moderatorCount = 2), + aRolesAndPermissionsState( + adminCount = 1, + moderatorCount = 2, + changeOwnRoleAction = AsyncAction.Confirming, + ), + aRolesAndPermissionsState( + adminCount = 1, + moderatorCount = 2, + changeOwnRoleAction = AsyncAction.Loading, + ), + aRolesAndPermissionsState( + adminCount = 1, + moderatorCount = 2, + changeOwnRoleAction = AsyncAction.Failure(IllegalStateException("Failed to change role")), + ), + ) +} + +internal fun aRolesAndPermissionsState( + adminCount: Int = 0, + moderatorCount: Int = 0, + changeOwnRoleAction: AsyncAction = AsyncAction.Uninitialized, + eventSink: (RolesAndPermissionsEvents) -> Unit = {}, +) = RolesAndPermissionsState( + adminCount = adminCount, + moderatorCount = moderatorCount, + changeOwnRoleAction = changeOwnRoleAction, + eventSink = eventSink, +) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsView.kt new file mode 100644 index 0000000000..087fa8c862 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsView.kt @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.rolesandpermissions + +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.res.stringResource +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.roomdetails.impl.R +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.designsystem.components.ProgressDialog +import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog +import io.element.android.libraries.designsystem.components.list.ListItemContent +import io.element.android.libraries.designsystem.components.preferences.PreferencePage +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.preview.sheetStateForPreview +import io.element.android.libraries.designsystem.theme.components.HorizontalDivider +import io.element.android.libraries.designsystem.theme.components.IconSource +import io.element.android.libraries.designsystem.theme.components.ListItem +import io.element.android.libraries.designsystem.theme.components.ListItemStyle +import io.element.android.libraries.designsystem.theme.components.ListSectionHeader +import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.hide +import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun RolesAndPermissionsView( + state: RolesAndPermissionsState, + roomDetailsAdminSettingsNavigator: RoomDetailsAdminSettingsNavigator, + modifier: Modifier = Modifier, +) { + PreferencePage( + modifier = modifier, + title = stringResource(R.string.screen_room_roles_and_permissions_title), + onBackPressed = roomDetailsAdminSettingsNavigator::onBackPressed, + ) { + ListSectionHeader(title = stringResource(R.string.screen_room_roles_and_permissions_roles_header), hasDivider = false) + ListItem( + headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_admins)) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Admin())), + trailingContent = ListItemContent.Text("${state.adminCount}"), + onClick = { roomDetailsAdminSettingsNavigator.openAdminList() }, + ) + ListItem( + headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_moderators)) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.ChatProblem())), + trailingContent = ListItemContent.Text("${state.moderatorCount}"), + onClick = { roomDetailsAdminSettingsNavigator.openModeratorList() }, + ) + ListItem( + headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_change_my_role)) }, + onClick = { state.eventSink(RolesAndPermissionsEvents.ChangeOwnRole) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Edit())) + ) + HorizontalDivider() + } + + when (state.changeOwnRoleAction) { + is AsyncAction.Confirming -> { + ChangeOwnRoleBottomSheet( + eventSink = state.eventSink, + ) + } + is AsyncAction.Loading -> { + ProgressDialog() + } + is AsyncAction.Failure -> { + ErrorDialog( + content = stringResource(CommonStrings.error_unknown), + onDismiss = { state.eventSink(RolesAndPermissionsEvents.CancelPendingAction) } + ) + } + else -> Unit + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ChangeOwnRoleBottomSheet( + eventSink: (RolesAndPermissionsEvents) -> Unit, +) { + val coroutineScope = rememberCoroutineScope() + val sheetState = if (LocalInspectionMode.current) { + sheetStateForPreview() + } else { + rememberModalBottomSheetState(skipPartiallyExpanded = true) + } + fun dismiss() { + sheetState.hide(coroutineScope) { + eventSink(RolesAndPermissionsEvents.CancelPendingAction) + } + } + ModalBottomSheet( + modifier = Modifier + .systemBarsPadding() + .navigationBarsPadding(), + sheetState = sheetState, + onDismissRequest = ::dismiss, + ) { + Text( + modifier = Modifier.padding(14.dp), + text = stringResource(R.string.screen_room_roles_and_permissions_change_my_role), + style = ElementTheme.typography.fontBodyLgMedium, + color = ElementTheme.colors.textPrimary, + ) + Text( + modifier = Modifier.padding(start = 14.dp, end = 14.dp, bottom = 16.dp), + text = stringResource(R.string.screen_room_change_role_confirm_demote_self_description), + style = ElementTheme.typography.fontBodyLgRegular, + color = ElementTheme.colors.textPrimary, + ) + ListItem( + headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_change_role_demote_to_moderator)) }, + onClick = { + sheetState.hide(coroutineScope) { + eventSink(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.MODERATOR)) + } + }, + style = ListItemStyle.Destructive, + ) + ListItem( + headlineContent = { Text(stringResource(R.string.screen_room_roles_and_permissions_change_role_demote_to_member)) }, + onClick = { + sheetState.hide(coroutineScope) { + eventSink(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.USER)) + } + }, + style = ListItemStyle.Destructive, + ) + ListItem( + headlineContent = { Text(stringResource(CommonStrings.action_cancel)) }, + onClick = { + sheetState.hide(coroutineScope) { + eventSink(RolesAndPermissionsEvents.CancelPendingAction) + } + }, + style = ListItemStyle.Primary, + ) + } +} + +@PreviewsDayNight +@Composable +internal fun RoomDetailsAdminSettingsViewPreview(@PreviewParameter(RolesAndPermissionsStateProvider::class) state: RolesAndPermissionsState) { + ElementPreview { + RolesAndPermissionsView( + state = state, + roomDetailsAdminSettingsNavigator = object : RoomDetailsAdminSettingsNavigator {}, + ) + } +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesEvent.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesEvent.kt new file mode 100644 index 0000000000..afb12a2e50 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesEvent.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles + +import io.element.android.libraries.matrix.api.room.RoomMember + +sealed interface ChangeRolesEvent { + data object ToggleSearchActive : ChangeRolesEvent + data class QueryChanged(val query: String?) : ChangeRolesEvent + data class UserSelectionToggled(val roomMember: RoomMember) : ChangeRolesEvent + data object Save : ChangeRolesEvent + data object Exit : ChangeRolesEvent + data object CancelExit : ChangeRolesEvent + data object ClearError : ChangeRolesEvent + data object CancelSave : ChangeRolesEvent +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesNode.kt new file mode 100644 index 0000000000..291beebe99 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesNode.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles + +import android.os.Parcelable +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.room.RoomMember +import kotlinx.parcelize.Parcelize + +@ContributesNode(RoomScope::class) +class ChangeRolesNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + presenterFactory: ChangeRolesPresenter.Factory, +) : Node(buildContext, plugins = plugins) { + sealed interface ListType : Parcelable { + @Parcelize + data object Admins : ListType + @Parcelize + data object Moderators : ListType + } + + @Parcelize + data class Inputs( + val listType: ListType, + ) : NodeInputs, Parcelable + + private val inputs: Inputs = inputs() + + private val presenter = presenterFactory.run { + val role = when (inputs.listType) { + is ListType.Admins -> RoomMember.Role.ADMIN + is ListType.Moderators -> RoomMember.Role.MODERATOR + } + create(role) + } + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + ChangeRolesView( + modifier = modifier, + state = state, + onBackPressed = this::navigateUp, + ) + } +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesPresenter.kt new file mode 100644 index 0000000000..1ab7985080 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesPresenter.kt @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.element.android.features.roomdetails.impl.members.PowerLevelRoomMemberComparator +import io.element.android.features.roomdetails.impl.members.RoomMemberListDataSource +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange +import io.element.android.libraries.matrix.api.user.MatrixUser +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch + +class ChangeRolesPresenter @AssistedInject constructor( + @Assisted private val role: RoomMember.Role, + private val room: MatrixRoom, + private val dispatchers: CoroutineDispatchers, +) : Presenter { + @AssistedFactory + interface Factory { + fun create(role: RoomMember.Role): ChangeRolesPresenter + } + + @Composable + override fun present(): ChangeRolesState { + val coroutineScope = rememberCoroutineScope() + val dataSource = remember { RoomMemberListDataSource(room, dispatchers) } + var query by rememberSaveable { mutableStateOf(null) } + var searchActive by rememberSaveable { mutableStateOf(false) } + var searchResults by remember { + mutableStateOf>>(SearchBarResultState.Initial()) + } + val selectedUsers = remember { + mutableStateOf>(persistentListOf()) + } + val exitState: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } + val saveState: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } + val usersWithRole = produceState(initialValue = persistentListOf()) { + room.usersWithRole(role) + .map { members -> members.map { it.toMatrixUser() } } + .onEach { users -> + val previous: PersistentList = value + value = users.toPersistentList() + // Users who were selected but didn't have the role, so their role change was pending + val toAdd = selectedUsers.value.filter { user -> users.none { it.userId == user.userId } && previous.none { it.userId == user.userId } } + // Users who no longer have the role + val toRemove = previous.filter { user -> users.none { it.userId == user.userId } } + selectedUsers.value = (users + toAdd - toRemove).toImmutableList() + } + .launchIn(this) + } + + val roomMemberState by room.membersStateFlow.collectAsState() + + // Update search results for every query change + LaunchedEffect(query, roomMemberState) { + val results = dataSource + .search(query.orEmpty()) + .sorted() + + searchResults = if (results.isEmpty()) { + SearchBarResultState.NoResultsFound() + } else { + SearchBarResultState.Results(results) + } + } + + val hasPendingChanges = usersWithRole.value != selectedUsers.value + + val roomInfo by room.roomInfoFlow.collectAsState(initial = null) + fun canChangeMemberRole(userId: UserId): Boolean { + // An admin can't remove or demote another admin + val powerLevel = roomInfo?.userPowerLevels?.get(userId) ?: 0L + return RoomMember.Role.forPowerLevel(powerLevel) != RoomMember.Role.ADMIN + } + + fun handleEvent(event: ChangeRolesEvent) { + when (event) { + is ChangeRolesEvent.ToggleSearchActive -> { + searchActive = !searchActive + } + is ChangeRolesEvent.QueryChanged -> { + query = event.query + } + is ChangeRolesEvent.UserSelectionToggled -> { + val newList = selectedUsers.value.toMutableList() + val index = newList.indexOfFirst { it.userId == event.roomMember.userId } + if (index >= 0) { + newList.removeAt(index) + } else { + newList.add(event.roomMember.toMatrixUser()) + } + selectedUsers.value = newList.toImmutableList() + } + is ChangeRolesEvent.Save -> { + if (role == RoomMember.Role.ADMIN && selectedUsers != usersWithRole && !saveState.value.isConfirming()) { + // Confirm adding admin + saveState.value = AsyncAction.Confirming + } else if (!saveState.value.isLoading()) { + coroutineScope.save(usersWithRole.value, selectedUsers, saveState) + } + } + is ChangeRolesEvent.ClearError -> { + saveState.value = AsyncAction.Uninitialized + } + is ChangeRolesEvent.Exit -> { + exitState.value = if (exitState.value.isUninitialized() && hasPendingChanges) { + // Has pending changes, confirm exit + AsyncAction.Confirming + } else { + // No pending changes, exit immediately + AsyncAction.Success(Unit) + } + } + is ChangeRolesEvent.CancelExit -> { + exitState.value = AsyncAction.Uninitialized + } + is ChangeRolesEvent.CancelSave -> { + saveState.value = AsyncAction.Uninitialized + } + } + } + return ChangeRolesState( + role = role, + query = query, + isSearchActive = searchActive, + searchResults = searchResults, + selectedUsers = selectedUsers.value, + hasPendingChanges = hasPendingChanges, + exitState = exitState.value, + savingState = saveState.value, + canChangeMemberRole = ::canChangeMemberRole, + eventSink = ::handleEvent, + ) + } + + private fun Iterable.sorted(): ImmutableList { + return sortedWith(PowerLevelRoomMemberComparator()).toImmutableList() + } + + private fun RoomMember.toMatrixUser() = MatrixUser( + userId = userId, + displayName = displayName, + avatarUrl = avatarUrl, + ) + + private fun CoroutineScope.save( + usersWithRole: ImmutableList, + selectedUsers: MutableState>, + saveState: MutableState>, + ) = launch { + saveState.value = AsyncAction.Loading + + val toAdd = selectedUsers.value - usersWithRole + val toRemove = usersWithRole - selectedUsers.value + + val changes: List = buildList { + for (selectedUser in toAdd) { + add(UserRoleChange(selectedUser.userId, role)) + } + for (selectedUser in toRemove) { + add(UserRoleChange(selectedUser.userId, RoomMember.Role.USER)) + } + } + + room.updateUsersRoles(changes) + .onFailure { + saveState.value = AsyncAction.Failure(it) + } + .onSuccess { + saveState.value = AsyncAction.Success(Unit) + } + } +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesState.kt new file mode 100644 index 0000000000..79a955df13 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesState.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles + +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.user.MatrixUser +import kotlinx.collections.immutable.ImmutableList + +data class ChangeRolesState( + val role: RoomMember.Role, + val query: String?, + val isSearchActive: Boolean, + val searchResults: SearchBarResultState>, + val selectedUsers: ImmutableList, + val hasPendingChanges: Boolean, + val exitState: AsyncAction, + val savingState: AsyncAction, + val canChangeMemberRole: (UserId) -> Boolean, + val eventSink: (ChangeRolesEvent) -> Unit, +) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesStateProvider.kt new file mode 100644 index 0000000000..69e8d72be6 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesStateProvider.kt @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.roomdetails.impl.members.aRoomMemberList +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.ui.components.aMatrixUserList +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +class ChangeRolesStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aChangeRolesState(), + aChangeRolesState(role = RoomMember.Role.MODERATOR), + aChangeRolesStateWithSelectedUsers().copy(hasPendingChanges = false), + aChangeRolesStateWithSelectedUsers(), + aChangeRolesStateWithSelectedUsers().copy( + selectedUsers = aMatrixUserList().take(2).toImmutableList(), + ), + aChangeRolesStateWithSelectedUsers().copy( + query = "Alice", + isSearchActive = true, + searchResults = SearchBarResultState.Results(aRoomMemberList().take(1).toImmutableList()), + selectedUsers = aMatrixUserList().take(1).toImmutableList(), + ), + aChangeRolesStateWithSelectedUsers().copy(exitState = AsyncAction.Confirming), + aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.Confirming), + aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.Loading), + aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.Success(Unit)), + aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.Failure(Exception("boom"))), + ) +} + +internal fun aChangeRolesState( + role: RoomMember.Role = RoomMember.Role.ADMIN, + query: String? = null, + isSearchActive: Boolean = false, + searchResults: SearchBarResultState> = SearchBarResultState.NoResultsFound(), + selectedUsers: ImmutableList = persistentListOf(), + hasPendingChanges: Boolean = false, + exitState: AsyncAction = AsyncAction.Uninitialized, + savingState: AsyncAction = AsyncAction.Uninitialized, + canRemoveMember: (UserId) -> Boolean = { true }, +) = ChangeRolesState( + role = role, + query = query, + isSearchActive = isSearchActive, + searchResults = searchResults, + selectedUsers = selectedUsers, + hasPendingChanges = hasPendingChanges, + exitState = exitState, + savingState = savingState, + canChangeMemberRole = canRemoveMember, + eventSink = {}, +) + +internal fun aChangeRolesStateWithSelectedUsers() = aChangeRolesState( + selectedUsers = aMatrixUserList().toImmutableList(), + searchResults = SearchBarResultState.Results(aRoomMemberList().toImmutableList()), + hasPendingChanges = true, + canRemoveMember = { it != UserId("@alice:server.org") }, +) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesView.kt new file mode 100644 index 0000000000..d7242014a6 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesView.kt @@ -0,0 +1,296 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.features.roomdetails.impl.R +import io.element.android.features.roomdetails.impl.members.aRoomMember +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.designsystem.components.ProgressDialog +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 +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.aliasScreenTitle +import io.element.android.libraries.designsystem.theme.components.Checkbox +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.SearchBar +import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +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.core.UserId +import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.ui.components.MatrixUserRow +import io.element.android.libraries.matrix.ui.components.SelectedUsersRowList +import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ChangeRolesView( + state: ChangeRolesState, + onBackPressed: () -> Unit, + modifier: Modifier = Modifier, +) { + val updatedOnBackPressed by rememberUpdatedState(newValue = onBackPressed) + BackHandler { + if (state.isSearchActive) { + state.eventSink(ChangeRolesEvent.ToggleSearchActive) + } else { + state.eventSink(ChangeRolesEvent.Exit) + } + } + + Box(modifier = modifier) { + Scaffold( + modifier = Modifier + .fillMaxSize() + .systemBarsPadding(), + topBar = { + AnimatedVisibility(visible = !state.isSearchActive) { + TopAppBar( + title = { + val title = when (state.role) { + RoomMember.Role.ADMIN -> stringResource(R.string.screen_room_change_role_administrators_title) + RoomMember.Role.MODERATOR -> stringResource(R.string.screen_room_change_role_moderators_title) + RoomMember.Role.USER -> error("This should never be reached") + } + Text( + text = title, + style = ElementTheme.typography.aliasScreenTitle, + ) + }, + navigationIcon = { + BackButton(onClick = { state.eventSink(ChangeRolesEvent.Exit) }) + }, + actions = { + TextButton( + text = stringResource(CommonStrings.action_save), + enabled = state.hasPendingChanges, + onClick = { state.eventSink(ChangeRolesEvent.Save) } + ) + } + ) + } + } + ) { paddingValues -> + Column( + modifier = Modifier.padding(paddingValues), + ) { + val lazyListState = rememberLazyListState() + SearchBar( + modifier = Modifier.padding(bottom = 16.dp), + placeHolderTitle = stringResource(CommonStrings.common_search_for_someone), + query = state.query.orEmpty(), + onQueryChange = { state.eventSink(ChangeRolesEvent.QueryChanged(it)) }, + active = state.isSearchActive, + onActiveChange = { state.eventSink(ChangeRolesEvent.ToggleSearchActive) }, + resultState = state.searchResults, + ) { members -> + SearchResultsList( + isSearchActive = true, + lazyListState = lazyListState, + searchResults = members, + selectedUsers = state.selectedUsers, + canRemoveMember = state.canChangeMemberRole, + onSelectionToggled = { state.eventSink(ChangeRolesEvent.UserSelectionToggled(it)) }, + selectedUsersList = {}, + ) + } + AnimatedVisibility( + visible = !state.isSearchActive, + enter = fadeIn(), + exit = fadeOut() + ) { + Column { + SearchResultsList( + isSearchActive = false, + lazyListState = lazyListState, + searchResults = (state.searchResults as? SearchBarResultState.Results)?.results ?: persistentListOf(), + selectedUsers = state.selectedUsers, + canRemoveMember = state.canChangeMemberRole, + onSelectionToggled = { state.eventSink(ChangeRolesEvent.UserSelectionToggled(it)) }, + selectedUsersList = { users -> + SelectedUsersRowList( + contentPadding = PaddingValues(start = 16.dp, end = 16.dp, bottom = 16.dp), + selectedUsers = users, + onUserRemoved = { + state.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(it.userId))) + }, + canDeselect = { state.canChangeMemberRole(it.userId) }, + ) + } + ) + } + } + } + } + + val asyncIndicatorState = rememberAsyncIndicatorState() + AsyncIndicatorHost(modifier = Modifier.statusBarsPadding(), asyncIndicatorState) + + when (state.exitState) { + is AsyncAction.Confirming -> { + ConfirmationDialog( + title = stringResource(CommonStrings.dialog_unsaved_changes_title), + content = stringResource(CommonStrings.dialog_unsaved_changes_description_android), + onSubmitClicked = { state.eventSink(ChangeRolesEvent.Exit) }, + onDismiss = { state.eventSink(ChangeRolesEvent.CancelExit) } + ) + } + is AsyncAction.Success -> { + SideEffect { updatedOnBackPressed() } + } + else -> Unit + } + + when (state.savingState) { + is AsyncAction.Confirming -> { + if (state.role == RoomMember.Role.ADMIN) { + // Confirm adding new admins dialogs + ConfirmationDialog( + title = stringResource(R.string.screen_room_change_role_confirm_add_admin_title), + content = stringResource(R.string.screen_room_change_role_confirm_add_admin_description), + onSubmitClicked = { state.eventSink(ChangeRolesEvent.Save) }, + onDismiss = { state.eventSink(ChangeRolesEvent.ClearError) } + ) + } + } + is AsyncAction.Loading -> { + ProgressDialog() + } + is AsyncAction.Failure -> { + ErrorDialog( + content = stringResource(CommonStrings.error_unknown), + onDismiss = { state.eventSink(ChangeRolesEvent.ClearError) } + ) + } + is AsyncAction.Success -> { + LaunchedEffect(state.savingState) { + asyncIndicatorState.enqueue(durationMs = AsyncIndicator.DURATION_SHORT) { + AsyncIndicator.Custom(text = stringResource(CommonStrings.common_saved_changes)) + } + } + } + else -> Unit + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun SearchResultsList( + isSearchActive: Boolean, + searchResults: ImmutableList, + selectedUsers: ImmutableList, + canRemoveMember: (UserId) -> Boolean, + onSelectionToggled: (RoomMember) -> Unit, + lazyListState: LazyListState, + selectedUsersList: @Composable (ImmutableList) -> Unit, +) { + LazyColumn( + state = lazyListState, + ) { + item { + selectedUsersList(selectedUsers) + } + stickyHeader { + val textResId = if (isSearchActive) { + CommonStrings.common_search_results + } else { + R.string.screen_room_member_list_room_members_header_title + } + Text( + modifier = Modifier + .background(ElementTheme.colors.bgCanvasDefault) + .padding(horizontal = 16.dp, vertical = 8.dp) + .fillMaxWidth(), + text = stringResource(textResId), + style = ElementTheme.typography.fontBodyLgMedium, + ) + } + items(searchResults, key = { it.userId }) { roomMember -> + val canToggle = canRemoveMember(roomMember.userId) + val trailingContent: @Composable (() -> Unit)? = if (canToggle) { + { + Checkbox( + checked = selectedUsers.any { it.userId == roomMember.userId }, + onCheckedChange = { onSelectionToggled(roomMember) }, + ) + } + } else { + null + } + MatrixUserRow( + modifier = Modifier.clickable(enabled = canToggle, onClick = { onSelectionToggled(roomMember) }), + matrixUser = MatrixUser( + userId = roomMember.userId, + displayName = roomMember.displayName, + avatarUrl = roomMember.avatarUrl, + ), + trailingContent = trailingContent, + ) + } + } +} + +@PreviewsDayNight +@Composable +internal fun ChangeRolesViewPreview(@PreviewParameter(ChangeRolesStateProvider::class) state: ChangeRolesState) { + ElementPreview { + ChangeRolesView( + state = state, + onBackPressed = {}, + ) + } +} diff --git a/features/roomdetails/impl/src/main/res/values/localazy.xml b/features/roomdetails/impl/src/main/res/values/localazy.xml index d606b5bc70..62a6671804 100644 --- a/features/roomdetails/impl/src/main/res/values/localazy.xml +++ b/features/roomdetails/impl/src/main/res/values/localazy.xml @@ -9,6 +9,29 @@ "An error occurred while updating the notification setting." "Your homeserver does not support this option in encrypted rooms, you may not get notified in some rooms." "Polls" + "Admins only" + "Ban people" + "Delete messages" + "Everyone" + "Invite people" + "Member moderation" + "Messages and content" + "Admins and moderators" + "Remove people" + "Change Room Avatar" + "Room details" + "Change Room Name" + "Change Room Topic" + "Send messages" + "Edit Admins" + "You will not be able to undo this action. You are promoting the user to have the same power level as you." + "Add Admin?" + "Demote" + "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges." + "Demote yourself?" + "Edit Moderators" + "You have unsaved changes." + "Save changes?" "Add topic" "Already a member" "Already invited" @@ -70,5 +93,17 @@ "All messages" "Mentions and Keywords only" "In this room, notify me for" + "Admins" + "Change my role" + "Demote to member" + "Demote to moderator" + "Member moderation" + "Messages and content" + "Moderators" + "Permissions" + "Reset permissions" + "Roles" + "Room details" + "Roles and permissions" "An error occurred when trying to start a chat" diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt index e51e3459d6..f4e7ef1d60 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt @@ -257,6 +257,7 @@ private fun AndroidComposeTestRule.setRoomD invitePeople: () -> Unit = EnsureNeverCalled(), openAvatarPreview: (name: String, url: String) -> Unit = EnsureNeverCalledWithTwoParams(), openPollHistory: () -> Unit = EnsureNeverCalled(), + openAdminSettings: () -> Unit = EnsureNeverCalled(), ) { setContent { RoomDetailsView( @@ -270,6 +271,7 @@ private fun AndroidComposeTestRule.setRoomD invitePeople = invitePeople, openAvatarPreview = openAvatarPreview, openPollHistory = openPollHistory, + openAdminSettings = openAdminSettings, ) } } diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/RolesAndPermissionPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/RolesAndPermissionPresenterTests.kt new file mode 100644 index 0000000000..0221a6851a --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/RolesAndPermissionPresenterTests.kt @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.rolesandpermissions + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsEvents +import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsPresenter +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class RolesAndPermissionPresenterTests { + @Test + fun `present - initial state`() = runTest { + val presenter = createRolesAndPermissionsPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + with(awaitItem()) { + assertThat(adminCount).isEqualTo(0) + assertThat(moderatorCount).isEqualTo(0) + assertThat(changeOwnRoleAction).isEqualTo(AsyncAction.Uninitialized) + } + } + } + + @Test + fun `present - ChangeOwnRole presents a confirmation dialog`() = runTest { + val presenter = createRolesAndPermissionsPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(RolesAndPermissionsEvents.ChangeOwnRole) + + assertThat(awaitItem().changeOwnRoleAction).isEqualTo(AsyncAction.Confirming) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `present - DemoteSelfTo changes own role to the specified one`() = runTest(StandardTestDispatcher()) { + val presenter = createRolesAndPermissionsPresenter(dispatchers = testCoroutineDispatchers()) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.MODERATOR)) + + runCurrent() + assertThat(awaitItem().changeOwnRoleAction).isEqualTo(AsyncAction.Loading) + + runCurrent() + assertThat(awaitItem().changeOwnRoleAction).isEqualTo(AsyncAction.Success(Unit)) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `present - DemoteSelfTo can handle failures and clean them`() = runTest(StandardTestDispatcher()) { + val room = FakeMatrixRoom().apply { + givenUpdateUserRoleResult(Result.failure(Exception("Failed to update role"))) + } + val presenter = createRolesAndPermissionsPresenter(room = room, dispatchers = testCoroutineDispatchers()) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.MODERATOR)) + + runCurrent() + assertThat(awaitItem().changeOwnRoleAction).isEqualTo(AsyncAction.Loading) + + runCurrent() + assertThat(awaitItem().changeOwnRoleAction).isInstanceOf(AsyncAction.Failure::class.java) + + initialState.eventSink(RolesAndPermissionsEvents.CancelPendingAction) + assertThat(awaitItem().changeOwnRoleAction).isEqualTo(AsyncAction.Uninitialized) + } + } + + @Test + fun `present - CancelPendingAction dismisses confirmation dialog too`() = runTest { + val presenter = createRolesAndPermissionsPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(RolesAndPermissionsEvents.ChangeOwnRole) + awaitItem().eventSink(RolesAndPermissionsEvents.CancelPendingAction) + + assertThat(awaitItem().changeOwnRoleAction).isEqualTo(AsyncAction.Uninitialized) + } + } + + private fun TestScope.createRolesAndPermissionsPresenter( + room: FakeMatrixRoom = FakeMatrixRoom(), + dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), + ): RolesAndPermissionsPresenter { + return RolesAndPermissionsPresenter(room = room, dispatchers = dispatchers) + } +} diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/RolesAndPermissionsViewTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/RolesAndPermissionsViewTests.kt new file mode 100644 index 0000000000..6eb596ce83 --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/RolesAndPermissionsViewTests.kt @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.rolesandpermissions + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.roomdetails.impl.R +import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsState +import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsView +import io.element.android.features.roomdetails.impl.rolesandpermissions.RoomDetailsAdminSettingsNavigator +import io.element.android.features.roomdetails.impl.rolesandpermissions.aRolesAndPermissionsState +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnce +import io.element.android.tests.testutils.pressBack +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class RolesAndPermissionsViewTests { + @get:Rule val rule = createAndroidComposeRule() + + @Test + fun `click on back invokes expected callback`() { + ensureCalledOnce { callback -> + rule.setRolesAndPermissionsView( + goBack = callback, + ) + rule.pressBack() + } + } + + @Test + fun `tapping on Admins opens admin list`() { + ensureCalledOnce { callback -> + rule.setRolesAndPermissionsView( + openAdminList = callback, + ) + rule.clickOn(R.string.screen_room_roles_and_permissions_admins) + } + } + + @Test + fun `tapping on Moderators opens moderators list`() { + ensureCalledOnce { callback -> + rule.setRolesAndPermissionsView( + openModeratorList = callback, + ) + rule.clickOn(R.string.screen_room_roles_and_permissions_moderators) + } + } +} + +private fun AndroidComposeTestRule.setRolesAndPermissionsView( + state: RolesAndPermissionsState = aRolesAndPermissionsState( + eventSink = EventsRecorder(expectEvents = false), + ), + goBack: () -> Unit = EnsureNeverCalled(), + openAdminList: () -> Unit = EnsureNeverCalled(), + openModeratorList: () -> Unit = EnsureNeverCalled(), +) { + setContent { + RolesAndPermissionsView( + state = state, + roomDetailsAdminSettingsNavigator = object : RoomDetailsAdminSettingsNavigator { + override fun onBackPressed() = goBack() + override fun openAdminList() = openAdminList() + override fun openModeratorList() = openModeratorList() + } + ) + } +} diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/changeroles/ChangeRolesPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/changeroles/ChangeRolesPresenterTests.kt new file mode 100644 index 0000000000..e7d3536cd3 --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/changeroles/ChangeRolesPresenterTests.kt @@ -0,0 +1,374 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.rolesandpermissions.changeroles + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.features.roomdetails.impl.members.aRoomMember +import io.element.android.features.roomdetails.impl.members.aRoomMemberList +import io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles.ChangeRolesEvent +import io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles.ChangeRolesPresenter +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState +import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.A_USER_ID_2 +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.aRoomInfo +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.collections.immutable.persistentMapOf +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class ChangeRolesPresenterTests { + @Test + fun `present - initial state`() = runTest { + val presenter = createChangeRolesPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + with(awaitItem()) { + assertThat(role).isEqualTo(RoomMember.Role.ADMIN) + assertThat(query).isNull() + assertThat(isSearchActive).isFalse() + assertThat(searchResults).isInstanceOf(SearchBarResultState.Initial::class.java) + assertThat(selectedUsers).isEmpty() + assertThat(hasPendingChanges).isFalse() + assertThat(exitState).isEqualTo(AsyncAction.Uninitialized) + assertThat(savingState).isEqualTo(AsyncAction.Uninitialized) + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - initial results are loaded automatically`() = runTest { + val room = FakeMatrixRoom().apply { + givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList())) + } + val presenter = createChangeRolesPresenter(room = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + assertThat(awaitItem().searchResults).isInstanceOf(SearchBarResultState.Results::class.java) + } + } + + @Test + fun `present - ToggleSearchActive changes the value`() = runTest { + val room = FakeMatrixRoom().apply { + givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList())) + } + val presenter = createChangeRolesPresenter(room = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + + initialState.eventSink(ChangeRolesEvent.ToggleSearchActive) + assertThat(awaitItem().isSearchActive).isTrue() + + initialState.eventSink(ChangeRolesEvent.ToggleSearchActive) + assertThat(awaitItem().isSearchActive).isFalse() + } + } + + @Test + fun `present - QueryChanged produces new results`() = runTest { + val room = FakeMatrixRoom().apply { + givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList())) + } + val presenter = createChangeRolesPresenter(room = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val initialResults = (awaitItem().searchResults as? SearchBarResultState.Results)?.results.orEmpty() + assertThat(initialResults).hasSize(10) + + initialState.eventSink(ChangeRolesEvent.QueryChanged("Alice")) + skipItems(1) + + val searchResults = (awaitItem().searchResults as? SearchBarResultState.Results)?.results.orEmpty() + assertThat(searchResults).hasSize(1) + assertThat(searchResults.firstOrNull()?.userId).isEqualTo(A_USER_ID) + } + } + + @Test + fun `present - changes in the room members produce new results`() = runTest { + val room = FakeMatrixRoom().apply { + givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList())) + } + val presenter = createChangeRolesPresenter(room = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialResults = (awaitItem().searchResults as? SearchBarResultState.Results)?.results.orEmpty() + assertThat(initialResults).hasSize(10) + + room.givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList().take(1).toPersistentList())) + skipItems(1) + + val searchResults = (awaitItem().searchResults as? SearchBarResultState.Results)?.results.orEmpty() + assertThat(searchResults).hasSize(1) + assertThat(searchResults.firstOrNull()?.userId).isEqualTo(A_USER_ID) + } + } + + @Test + fun `present - UserSelectionToggle adds and removes users from the selected user list`() = runTest { + val room = FakeMatrixRoom().apply { + givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList())) + givenRoomInfo(aRoomInfo(userPowerLevels = persistentMapOf(A_USER_ID to 100))) + } + val presenter = createChangeRolesPresenter(room = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.selectedUsers).hasSize(1) + + initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2))) + assertThat(awaitItem().selectedUsers).hasSize(2) + + initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2))) + assertThat(awaitItem().selectedUsers).hasSize(1) + } + } + + @Test + fun `present - hasPendingChanges is true when the initial selected users don't match the new ones`() = runTest { + val room = FakeMatrixRoom().apply { + givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList())) + givenRoomInfo(aRoomInfo(userPowerLevels = persistentMapOf(A_USER_ID to 100))) + } + val presenter = createChangeRolesPresenter(room = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.hasPendingChanges).isFalse() + assertThat(initialState.selectedUsers).hasSize(1) + + initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2))) + with(awaitItem()) { + assertThat(selectedUsers).hasSize(2) + assertThat(hasPendingChanges).isTrue() + } + + initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2))) + with(awaitItem()) { + assertThat(selectedUsers).hasSize(1) + assertThat(hasPendingChanges).isFalse() + } + } + } + + @Test + fun `present - Exit will display success if no pending changes`() = runTest { + val room = FakeMatrixRoom().apply { + givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList())) + givenRoomInfo(aRoomInfo(userPowerLevels = persistentMapOf(A_USER_ID to 100))) + } + val presenter = createChangeRolesPresenter(room = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.hasPendingChanges).isFalse() + assertThat(initialState.exitState).isEqualTo(AsyncAction.Uninitialized) + + initialState.eventSink(ChangeRolesEvent.Exit) + assertThat(awaitItem().exitState).isEqualTo(AsyncAction.Success(Unit)) + } + } + + @Test + fun `present - CancelExit will remove exit confirmation`() = runTest { + val room = FakeMatrixRoom().apply { + givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList())) + givenRoomInfo(aRoomInfo(userPowerLevels = persistentMapOf(A_USER_ID to 100))) + } + val presenter = createChangeRolesPresenter(room = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.hasPendingChanges).isFalse() + assertThat(initialState.exitState).isEqualTo(AsyncAction.Uninitialized) + + initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2))) + + awaitItem().eventSink(ChangeRolesEvent.Exit) + val confirmingState = awaitItem() + assertThat(confirmingState.exitState).isEqualTo(AsyncAction.Confirming) + + confirmingState.eventSink(ChangeRolesEvent.CancelExit) + assertThat(awaitItem().exitState).isEqualTo(AsyncAction.Uninitialized) + } + } + + @Test + fun `present - Exit will display a confirmation dialog if there are pending changes, calling it again will actually exit`() = runTest { + val room = FakeMatrixRoom().apply { + givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList())) + givenRoomInfo(aRoomInfo(userPowerLevels = persistentMapOf(A_USER_ID to 100))) + } + val presenter = createChangeRolesPresenter(room = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.hasPendingChanges).isFalse() + assertThat(initialState.exitState).isEqualTo(AsyncAction.Uninitialized) + + initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2))) + val updatedState = awaitItem() + assertThat(updatedState.hasPendingChanges).isTrue() + skipItems(1) + + updatedState.eventSink(ChangeRolesEvent.Exit) + assertThat(awaitItem().exitState).isEqualTo(AsyncAction.Confirming) + + updatedState.eventSink(ChangeRolesEvent.Exit) + assertThat(awaitItem().exitState).isEqualTo(AsyncAction.Success(Unit)) + } + } + + @Test + fun `present - Save will display a confirmation when adding admins`() = runTest { + val room = FakeMatrixRoom().apply { + givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList())) + givenRoomInfo(aRoomInfo(userPowerLevels = persistentMapOf(A_USER_ID to 100))) + } + val presenter = createChangeRolesPresenter(role = RoomMember.Role.ADMIN, room = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.selectedUsers).hasSize(1) + + initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2))) + + awaitItem().eventSink(ChangeRolesEvent.Save) + val confirmingState = awaitItem() + assertThat(confirmingState.savingState).isEqualTo(AsyncAction.Confirming) + + confirmingState.eventSink(ChangeRolesEvent.Save) + assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Success(Unit)) + } + } + + @Test + fun `present - CancelSave will remove the confirmation dialog`() = runTest { + val room = FakeMatrixRoom().apply { + givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList())) + givenRoomInfo(aRoomInfo(userPowerLevels = persistentMapOf(A_USER_ID to 100))) + } + val presenter = createChangeRolesPresenter(role = RoomMember.Role.ADMIN, room = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.selectedUsers).hasSize(1) + + initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2))) + + awaitItem().eventSink(ChangeRolesEvent.Save) + val confirmingState = awaitItem() + assertThat(confirmingState.savingState).isEqualTo(AsyncAction.Confirming) + + confirmingState.eventSink(ChangeRolesEvent.CancelSave) + assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Uninitialized) + } + } + + @Test + fun `present - Save will just save the data for moderators`() = runTest { + val room = FakeMatrixRoom().apply { + givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList())) + givenRoomInfo(aRoomInfo(userPowerLevels = persistentMapOf(A_USER_ID to 50))) + } + val presenter = createChangeRolesPresenter(role = RoomMember.Role.MODERATOR, room = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.selectedUsers).hasSize(1) + + initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2))) + + awaitItem().eventSink(ChangeRolesEvent.Save) + assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Success(Unit)) + } + } + + @Test + fun `present - Save can handle failures and ClearError clears them`() = runTest { + val room = FakeMatrixRoom().apply { + givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList())) + givenRoomInfo(aRoomInfo(userPowerLevels = persistentMapOf(A_USER_ID to 50))) + givenUpdateUserRoleResult(Result.failure(IllegalStateException("Failed"))) + } + val presenter = createChangeRolesPresenter(role = RoomMember.Role.MODERATOR, room = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.selectedUsers).hasSize(1) + + initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2))) + + awaitItem().eventSink(ChangeRolesEvent.Save) + val failedState = awaitItem() + assertThat(failedState.savingState).isInstanceOf(AsyncAction.Failure::class.java) + + failedState.eventSink(ChangeRolesEvent.ClearError) + assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Uninitialized) + } + } + + private fun TestScope.createChangeRolesPresenter( + role: RoomMember.Role = RoomMember.Role.ADMIN, + room: FakeMatrixRoom = FakeMatrixRoom(), + dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), + ): ChangeRolesPresenter { + return ChangeRolesPresenter( + role = role, + room = room, + dispatchers = dispatchers, + ) + } +} diff --git a/features/roomlist/impl/src/main/res/values/localazy.xml b/features/roomlist/impl/src/main/res/values/localazy.xml index 08efd3a79f..210ed2fb7f 100644 --- a/features/roomlist/impl/src/main/res/values/localazy.xml +++ b/features/roomlist/impl/src/main/res/values/localazy.xml @@ -8,10 +8,19 @@ "Get started by messaging someone." "No chats yet." "Favourites" + "You can add a chat to your favourites in the chat settings. +For now, you can deselect filters in order to see your other chats" + "You don’t have favourite chats yet" "Low Priority" + "You can deselect filters in order to see your other chats" + "You don’t have chats for this selection" "People" + "You don’t have any DMs yet" "Rooms" + "You’re not in any room yet" "Unreads" + "Congrats! +You don’t have any unread message!" "Chats" "Mark as read" "Mark as unread" diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index fd8f76b6e9..efe3107b31 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -29,12 +29,19 @@ import io.element.android.libraries.matrix.api.media.MediaUploadHandler import io.element.android.libraries.matrix.api.media.VideoInfo import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.room.location.AssetType +import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import java.io.Closeable import java.io.File @@ -56,7 +63,7 @@ interface MatrixRoom : Closeable { /** Whether the room is a direct message. */ val isDm: Boolean get() = isDirect && isOneToOne - val roomInfoFlow: Flow + val roomInfoFlow: SharedFlow val roomTypingMembersFlow: Flow> /** @@ -91,6 +98,10 @@ interface MatrixRoom : Closeable { suspend fun unsubscribeFromSync() + suspend fun userRole(userId: UserId): Result + + suspend fun updateUsersRoles(changes: List): Result + suspend fun userDisplayName(userId: UserId): Result suspend fun userAvatarUrl(userId: UserId): Result @@ -144,6 +155,18 @@ interface MatrixRoom : Closeable { suspend fun canUserJoinCall(userId: UserId): Result = canUserSendState(userId, StateEventType.CALL_MEMBER) + fun usersWithRole(role: RoomMember.Role): Flow> { + return roomInfoFlow + .map { it.userPowerLevels.filter { (_, powerLevel) -> RoomMember.Role.forPowerLevel(powerLevel) == role } } + .distinctUntilChanged() + .combine(membersStateFlow) { powerLevels, membersState -> + membersState.roomMembers() + .orEmpty() + .filter { powerLevels.containsKey(it.userId) } + .toPersistentList() + } + } + suspend fun updateAvatar(mimeType: String, data: ByteArray): Result suspend fun removeAvatar(): Result diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomInfo.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomInfo.kt index c35154798b..8ad8260a7d 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomInfo.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomInfo.kt @@ -17,8 +17,10 @@ package io.element.android.libraries.matrix.api.room import androidx.compose.runtime.Immutable +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableMap @Immutable data class MatrixRoomInfo( @@ -39,6 +41,7 @@ data class MatrixRoomInfo( val activeMembersCount: Long, val invitedMembersCount: Long, val joinedMembersCount: Long, + val userPowerLevels: ImmutableMap, val highlightCount: Long, val notificationCount: Long, val userDefinedNotificationMode: RoomNotificationMode?, diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt index 646755ee39..60f4102234 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt @@ -32,10 +32,20 @@ data class RoomMember( /** * Role of the RoomMember, based on its [powerLevel]. */ - enum class Role { - ADMIN, - MODERATOR, - USER + enum class Role(val powerLevel: Long) { + ADMIN(100L), + MODERATOR(50L), + USER(0L); + + companion object { + fun forPowerLevel(powerLevel: Long): Role { + return when { + powerLevel >= ADMIN.powerLevel -> ADMIN + powerLevel >= MODERATOR.powerLevel -> MODERATOR + else -> USER + } + } + } } /** diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/UserRoleChange.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/UserRoleChange.kt new file mode 100644 index 0000000000..65a6412b07 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/UserRoleChange.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.api.room.powerlevels + +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.RoomMember + +data class UserRoleChange( + val userId: UserId, + val role: RoomMember.Role, +) { + val powerLevel: Long = role.powerLevel +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapper.kt index 47f3b8a737..3331771f79 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapper.kt @@ -16,12 +16,15 @@ package io.element.android.libraries.matrix.impl.room +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper +import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentMap import org.matrix.rustcomponents.sdk.use import org.matrix.rustcomponents.sdk.Membership as RustMembership import org.matrix.rustcomponents.sdk.RoomInfo as RustRoomInfo @@ -49,6 +52,7 @@ class MatrixRoomInfoMapper( activeMembersCount = it.activeMembersCount.toLong(), invitedMembersCount = it.invitedMembersCount.toLong(), joinedMembersCount = it.joinedMembersCount.toLong(), + userPowerLevels = mapPowerLevels(it.userPowerLevels), highlightCount = it.highlightCount.toLong(), notificationCount = it.notificationCount.toLong(), userDefinedNotificationMode = it.userDefinedNotificationMode?.map(), @@ -69,3 +73,7 @@ fun RustRoomNotificationMode.map(): RoomNotificationMode = when (this) { RustRoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY RustRoomNotificationMode.MUTE -> RoomNotificationMode.MUTE } + +fun mapPowerLevels(powerLevels: Map): ImmutableMap { + return powerLevels.mapKeys { (key, _) -> UserId(key) }.toPersistentMap() +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 196d5bd715..c3a543cf60 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -36,8 +36,10 @@ import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.MatrixRoomNotificationSettingsState import io.element.android.libraries.matrix.api.room.Mention import io.element.android.libraries.matrix.api.room.MessageEventType +import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.location.AssetType +import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange import io.element.android.libraries.matrix.api.room.roomNotificationSettings import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.ReceiptType @@ -51,6 +53,7 @@ import io.element.android.libraries.matrix.impl.notificationsettings.RustNotific import io.element.android.libraries.matrix.impl.poll.toInner import io.element.android.libraries.matrix.impl.room.location.toInner import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher +import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline import io.element.android.libraries.matrix.impl.timeline.toRustReceiptType import io.element.android.libraries.matrix.impl.util.mxCallbackFlow @@ -63,8 +66,11 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.EventTimelineItem @@ -74,6 +80,7 @@ import org.matrix.rustcomponents.sdk.RoomListItem import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle import org.matrix.rustcomponents.sdk.TypingNotificationsListener +import org.matrix.rustcomponents.sdk.UserPowerLevelUpdate import org.matrix.rustcomponents.sdk.WidgetCapabilities import org.matrix.rustcomponents.sdk.WidgetCapabilitiesProvider import org.matrix.rustcomponents.sdk.messageEventContentFromHtml @@ -102,7 +109,7 @@ class RustMatrixRoom( ) : MatrixRoom { override val roomId = RoomId(innerRoom.id()) - override val roomInfoFlow: Flow = mxCallbackFlow { + override val roomInfoFlow: SharedFlow = mxCallbackFlow { launch { val initial = innerRoom.roomInfo().use(matrixRoomInfoMapper::map) channel.trySend(initial) @@ -113,6 +120,7 @@ class RustMatrixRoom( } }) } + .shareIn(sessionCoroutineScope, SharingStarted.Eagerly, replay = 1) override val roomTypingMembersFlow: Flow> = mxCallbackFlow { launch { @@ -228,6 +236,19 @@ class RustMatrixRoom( } } + override suspend fun userRole(userId: UserId): Result = withContext(coroutineDispatchers.io) { + runCatching { + RoomMemberMapper.mapRole(innerRoom.suggestedRoleForUser(userId.value)) + } + } + + override suspend fun updateUsersRoles(changes: List): Result { + return runCatching { + val powerLevelChanges = changes.map { UserPowerLevelUpdate(it.userId.value, it.powerLevel) } + innerRoom.updatePowerLevelsForUsers(powerLevelChanges) + } + } + override suspend fun userAvatarUrl(userId: UserId): Result = withContext(roomDispatcher) { runCatching { innerRoom.memberAvatarUrl(userId.value) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index 5ab6c621a5..c6eaa41462 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -40,6 +40,7 @@ import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.location.AssetType +import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem @@ -54,11 +55,14 @@ import io.element.android.libraries.matrix.test.notificationsettings.FakeNotific import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver import io.element.android.tests.testutils.simulateLongTask +import kotlinx.collections.immutable.ImmutableMap +import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import java.io.File @@ -86,6 +90,7 @@ class FakeMatrixRoom( private var unignoreResult: Result = Result.success(Unit) private var userDisplayNameResult = Result.success(null) private var userAvatarUrlResult = Result.success(null) + private var userRoleResult = Result.success(RoomMember.Role.USER) private var updateMembersResult: Result = Result.success(Unit) private var joinRoomResult = Result.success(Unit) private var inviteUserResult = Result.success(Unit) @@ -100,6 +105,7 @@ class FakeMatrixRoom( private var setTopicResult = Result.success(Unit) private var updateAvatarResult = Result.success(Unit) private var removeAvatarResult = Result.success(Unit) + private var updateUserRoleResult = Result.success(Unit) private var toggleReactionResult = Result.success(Unit) private var retrySendMessageResult = Result.success(Unit) private var cancelSendResult = Result.success(Unit) @@ -170,7 +176,7 @@ class FakeMatrixRoom( private var leaveRoomError: Throwable? = null private val _roomInfoFlow: MutableSharedFlow = MutableSharedFlow(replay = 1) - override val roomInfoFlow: Flow = _roomInfoFlow + override val roomInfoFlow: SharedFlow = _roomInfoFlow private val _roomTypingMembersFlow: MutableSharedFlow> = MutableSharedFlow(replay = 1) override val roomTypingMembersFlow: Flow> = _roomTypingMembersFlow @@ -206,6 +212,14 @@ class FakeMatrixRoom( userAvatarUrlResult } + override suspend fun userRole(userId: UserId): Result { + return userRoleResult + } + + override suspend fun updateUsersRoles(changes: List): Result { + return updateUserRoleResult + } + override suspend fun sendMessage(body: String, htmlBody: String?, mentions: List) = simulateLongTask { sendMessageMentions = mentions Result.success(Unit) @@ -496,6 +510,14 @@ class FakeMatrixRoom( userAvatarUrlResult = avatarUrl } + fun givenUserRoleResult(role: Result) { + userRoleResult = role + } + + fun givenUpdateUserRoleResult(result: Result) { + updateUserRoleResult = result + } + fun givenJoinRoomResult(result: Result) { joinRoomResult = result } @@ -668,6 +690,7 @@ fun aRoomInfo( notificationCount: Long = 0, userDefinedNotificationMode: RoomNotificationMode? = null, hasRoomCall: Boolean = false, + userPowerLevels: ImmutableMap = persistentMapOf(), activeRoomCallParticipants: List = emptyList() ) = MatrixRoomInfo( id = id, @@ -691,5 +714,6 @@ fun aRoomInfo( notificationCount = notificationCount, userDefinedNotificationMode = userDefinedNotificationMode, hasRoomCall = hasRoomCall, + userPowerLevels = userPowerLevels, activeRoomCallParticipants = activeRoomCallParticipants.toImmutableList(), ) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedUser.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedUser.kt index eb2391e655..3d9227ed46 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedUser.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedUser.kt @@ -51,6 +51,7 @@ import io.element.android.libraries.ui.strings.CommonStrings @Composable fun SelectedUser( matrixUser: MatrixUser, + canRemove: Boolean, onUserRemoved: (MatrixUser) -> Unit, modifier: Modifier = Modifier, ) { @@ -70,24 +71,26 @@ fun SelectedUser( style = MaterialTheme.typography.bodyLarge, ) } - Surface( - color = MaterialTheme.colorScheme.primary, - modifier = Modifier - .clip(CircleShape) - .size(20.dp) - .align(Alignment.TopEnd) - .clickable( - indication = rememberRipple(), - interactionSource = remember { MutableInteractionSource() }, - onClick = { onUserRemoved(matrixUser) } - ), - ) { - Icon( - imageVector = CompoundIcons.Close(), - contentDescription = stringResource(id = CommonStrings.action_remove), - tint = MaterialTheme.colorScheme.onPrimary, - modifier = Modifier.padding(2.dp) - ) + if (canRemove) { + Surface( + color = MaterialTheme.colorScheme.primary, + modifier = Modifier + .clip(CircleShape) + .size(20.dp) + .align(Alignment.TopEnd) + .clickable( + indication = rememberRipple(), + interactionSource = remember { MutableInteractionSource() }, + onClick = { onUserRemoved(matrixUser) } + ), + ) { + Icon( + imageVector = CompoundIcons.Close(), + contentDescription = stringResource(id = CommonStrings.action_remove), + tint = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier.padding(2.dp) + ) + } } } } @@ -97,6 +100,17 @@ fun SelectedUser( internal fun SelectedUserPreview() = ElementPreview { SelectedUser( aMatrixUser(), + canRemove = true, + onUserRemoved = {}, + ) +} + +@PreviewsDayNight +@Composable +internal fun SelectedUserCannotRemovePreview() = ElementPreview { + SelectedUser( + aMatrixUser(), + canRemove = false, onUserRemoved = {}, ) } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedUsersList.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedUsersRowList.kt similarity index 94% rename from libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedUsersList.kt rename to libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedUsersRowList.kt index e3aab57059..9c075f5051 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedUsersList.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedUsersRowList.kt @@ -46,11 +46,12 @@ import kotlinx.collections.immutable.toImmutableList import kotlin.math.floor @Composable -fun SelectedUsersList( +fun SelectedUsersRowList( selectedUsers: ImmutableList, onUserRemoved: (MatrixUser) -> Unit, modifier: Modifier = Modifier, autoScroll: Boolean = false, + canDeselect: (MatrixUser) -> Boolean = { true }, contentPadding: PaddingValues = PaddingValues(0.dp), ) { val lazyListState = rememberLazyListState() @@ -105,11 +106,12 @@ fun SelectedUsersList( .fillMaxWidth(), contentPadding = contentPadding, ) { - itemsIndexed(selectedUsers.toList()) { index, matrixUser -> + itemsIndexed(selectedUsers.toList()) { index, selectedUser -> Layout( content = { SelectedUser( - matrixUser = matrixUser, + matrixUser = selectedUser, + canRemove = canDeselect(selectedUser), onUserRemoved = onUserRemoved, ) }, @@ -133,7 +135,7 @@ fun SelectedUsersList( internal fun SelectedUsersListPreview() = ElementPreview { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { // Two users that will be visible with no scrolling - SelectedUsersList( + SelectedUsersRowList( selectedUsers = aMatrixUserList().take(2).toImmutableList(), onUserRemoved = {}, modifier = Modifier @@ -143,7 +145,7 @@ internal fun SelectedUsersListPreview() = ElementPreview { // Multiple users that don't fit, so will be spaced out per the measure policy for (i in 0..5) { - SelectedUsersList( + SelectedUsersRowList( selectedUsers = aMatrixUserList().take(6).toImmutableList(), onUserRemoved = {}, modifier = Modifier diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomState.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomState.kt index fbe35742e5..4a80709d42 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomState.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomState.kt @@ -18,9 +18,12 @@ package io.element.android.libraries.matrix.ui.room import androidx.compose.runtime.Composable import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MessageEventType +import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn import io.element.android.libraries.matrix.api.room.powerlevels.canSendMessage @@ -45,3 +48,10 @@ fun MatrixRoom.canRedactOtherAsState(updateKey: Long): State { value = canRedactOther().getOrElse { false } } } + +@Composable +fun MatrixRoom.isOwnUserAdmin(): Boolean { + val roomInfo by roomInfoFlow.collectAsState(initial = null) + val powerLevel = roomInfo?.userPowerLevels?.get(sessionId) ?: 0L + return RoomMember.Role.forPowerLevel(powerLevel) == RoomMember.Role.ADMIN +} diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index dba11ea811..b1442dcc51 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -57,6 +57,7 @@ "Enter PIN" "Forgot password?" "Forward" + "Go back" "Invite" "Invite people" "Invite people to %1$s" @@ -181,6 +182,7 @@ "Room" "Room name" "e.g. your project name" + "Saved changes" "Saving" "Screen lock" "Search for someone" @@ -224,6 +226,8 @@ "Error" "Success" "Warning" + "Your changes have not been saved. Are you sure you want to go back?" + "Save changes?" "Failed creating the permalink" "%1$s could not load the map. Please try again later." "Failed loading messages" diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_UserListView_null_UserListView-Day-2_3_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_UserListView_null_UserListView-Day-2_3_null_5,NEXUS_5,1.0,en].png index 2394e6b81c..2f2ac0e0c0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_UserListView_null_UserListView-Day-2_3_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_UserListView_null_UserListView-Day-2_3_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:71f571e4eba41cc46a2bf98a16c174af08e74ee85d598653579585d2673b6537 -size 64640 +oid sha256:3764d8bd7dc2783a8af43aad65a217d7e533ed17c4d4367b7994470bf35b62b0 +size 4462 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_UserListView_null_UserListView-Day-2_3_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_UserListView_null_UserListView-Day-2_3_null_6,NEXUS_5,1.0,en].png index b443c1fdd0..2f2ac0e0c0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_UserListView_null_UserListView-Day-2_3_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_UserListView_null_UserListView-Day-2_3_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f03c879cdfb8db6a0af85ab243e06f37626f9a78ec358b50a933ca1f873fd9ea -size 69323 +oid sha256:3764d8bd7dc2783a8af43aad65a217d7e533ed17c4d4367b7994470bf35b62b0 +size 4462 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_UserListView_null_UserListView-Night-2_4_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_UserListView_null_UserListView-Night-2_4_null_5,NEXUS_5,1.0,en].png index a8cf7f9b51..2f2ac0e0c0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_UserListView_null_UserListView-Night-2_4_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_UserListView_null_UserListView-Night-2_4_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0d47c2589e9ce06774122be1ccde68066fe5ddd513912745b2976a5aa42c4c5 -size 63805 +oid sha256:3764d8bd7dc2783a8af43aad65a217d7e533ed17c4d4367b7994470bf35b62b0 +size 4462 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_UserListView_null_UserListView-Night-2_4_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_UserListView_null_UserListView-Night-2_4_null_6,NEXUS_5,1.0,en].png index cdbde8dc99..2f2ac0e0c0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_UserListView_null_UserListView-Night-2_4_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_UserListView_null_UserListView-Night-2_4_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4300c9797ece72ff8324792233ffe24d94b38dc5b03be4100feb9b610d627257 -size 67998 +oid sha256:3764d8bd7dc2783a8af43aad65a217d7e533ed17c4d4367b7994470bf35b62b0 +size 4462 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-8_9_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-8_9_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..364d5ad96e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-8_9_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3cf713ae66135179f17b52578bf4663bf34a102b92c0809c42e5285856d43f3b +size 18257 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-8_9_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-8_9_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..6705985509 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-8_9_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7737c502737b811720b54ba8e8db4248df8e0e8577435c61270853f92e65f6bc +size 19731 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-8_9_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-8_9_null_10,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..8e48dd4ae9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-8_9_null_10,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b0ca2b91bc525254ed4be526e622608f77d809a1c097ef02943d4cd751c65b2 +size 55941 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-8_9_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-8_9_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..205b4fe623 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-8_9_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e96824d27d0fda0b183e8c90d659406d012861ff520f81cfa60374424137bbdb +size 66079 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-8_9_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-8_9_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d24aa84010 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-8_9_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b00d18ba4fa132e32c6580682c6b78d754d330735afcf8e4242ee0def213ba44 +size 66046 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-8_9_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-8_9_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..0eb57ff9d0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-8_9_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eeabb0a4ed645c7f47da62e7bafaca54ab1d3c5c48a195b48c3f807e5137d39c +size 59992 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-8_9_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-8_9_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..40bc944f5a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-8_9_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5199d1491a315d7653b5e09049ead901650332deb50ec4d24742ac7f3f7ff535 +size 15618 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-8_9_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-8_9_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..11c7296257 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-8_9_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54461c606debb6867bd48a054f551dd56e2895530f06e6890b44ad7b1eeb9219 +size 65289 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-8_9_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-8_9_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b5a534c0d2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-8_9_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:66707e11931cc5824142a5f42ed95c4becd5621ea3a9c9cac3df5f0435b3883b +size 66191 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-8_9_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-8_9_null_8,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d74825b1b7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-8_9_null_8,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31dcb5cef9f857e2b96631b4dd23a59a200c9174f8cc0e406d2a859497a8f893 +size 58934 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-8_9_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-8_9_null_9,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..7f712e5d7d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-8_9_null_9,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:56e0b337fc6afdadf477785df479478ae24dd7c9cb1f98bedf33b920ad374e78 +size 69899 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-8_10_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-8_10_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9c99ff605b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-8_10_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a99d564c528dbcc2080b6f83ca09a97bd518412019d0f32b76b80086af4f17e +size 16966 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-8_10_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-8_10_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..5e3ca5e603 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-8_10_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb3c08d58fd0dfa60ad1549b26a53f02239f1ca43aeda9e52350952d86d5fa74 +size 18178 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-8_10_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-8_10_null_10,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9cd1a27f5a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-8_10_null_10,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f86fad7b780fb7fc0b36377724c6830d86c5d41dcfd50f8de8cbe9f40473e68d +size 52719 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-8_10_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-8_10_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..afbfa647bf --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-8_10_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:00c4dba57e32a87d6acde138625214bb795c7e7c8987561d6ddd53fbf26674b5 +size 64446 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-8_10_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-8_10_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..cc77b40b16 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-8_10_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c3e270adac07c800c5ab1a124d564beee96b2e39e9473a69b8b90ff647bfd6df +size 64184 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-8_10_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-8_10_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..7615e4d3de --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-8_10_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6490597e92bd937451dbcf7d516f43525b5d18835cefec4f825fdf90ee6dc626 +size 57891 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-8_10_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-8_10_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d0d83c7d16 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-8_10_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5e088b944b00b462fcbaa16033a786835b8a09dd26f8a9b946580a237d2b363e +size 15064 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-8_10_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-8_10_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4eaec1e29e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-8_10_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:afac0f9958320d5c1f12d9b6647a15608eff51627dcb82d35ec9524a579f2637 +size 61111 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-8_10_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-8_10_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..aad0cbf857 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-8_10_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3991def6f61df49c73e025a417680f1bd409d8e2aff41ba2434e3a5a50914665 +size 62128 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-8_10_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-8_10_null_8,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..247ab0e9a6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-8_10_null_8,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af3c14da68316feb82b4ae905868116239e43cb8b0e5393246efe0edd61f1bc2 +size 56497 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-8_10_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-8_10_null_9,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ad76d6153e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-8_10_null_9,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1979b5b0dff66a05765a1ecd03d9a896d2aaf6dffc686ffd27bba3d5a0ac927a +size 67254 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Day-7_8_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Day-7_8_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..3c15ca411d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Day-7_8_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5c6410198fc2f7e74279e21013718bf8326bfd871fa7ffa244470725f7bf3ae5 +size 23045 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Day-7_8_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Day-7_8_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a4329e47f8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Day-7_8_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d6fce3b7fa9ac12e25f135ddeabb96dad4bba9422926e5451647255a712e481 +size 22772 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Day-7_8_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Day-7_8_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..26c069b100 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Day-7_8_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e079ab22491039912b9035eec6574a0ccd3f4f53344c3b7f14289da024fe703 +size 45054 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Day-7_8_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Day-7_8_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..944ddb606f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Day-7_8_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c788eab0495478d8253cbd11970c57c83670e2b761a675b413642db3c6ee57fa +size 25095 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Day-7_8_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Day-7_8_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e3cefb87a5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Day-7_8_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:98f81fcfa9cb5c7c02f8322ca4e444df32e81acb5f0d79f39841f9b71c436594 +size 26376 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Night-7_9_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Night-7_9_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f6fa9fb31f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Night-7_9_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dbc67150e8d533fd473204fbf4c9360751b8498831c435a06e9ed1525dfa1d7b +size 21637 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Night-7_9_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Night-7_9_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..16255be86e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Night-7_9_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:09b42b37d3e05cfe60187512403139b305c39d4d3ff59d122d73762a548d01fa +size 21364 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Night-7_9_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Night-7_9_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c4b0604264 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Night-7_9_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d384b4e18072f3e78e5dd1b76633908df176c665d40cb1765fe2daa8a86a0044 +size 39479 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Night-7_9_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Night-7_9_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4426f04cce --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Night-7_9_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:50b1f5c23b436ca2f6045c4d2a6d14c7902a5f10646ce59522a02c2edc297374 +size 22310 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Night-7_9_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Night-7_9_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..06fd4deac9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Night-7_9_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b7f91366d3576553c7dd2bc469290fa094dc4095510bb5e841fa0a9b179e85d +size 23085 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_0,NEXUS_5,1.0,en].png index 2e0d2f52f4..a9aa413768 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:10c67ba7b16d81e030cb44719cc68a0a3fd57acba75e488a37c3bdf4f739cf95 -size 44537 +oid sha256:fe5642aa6804a99d9b3505bb63fe0c328015592f35e095415c910d9a5abfa052 +size 47238 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_1,NEXUS_5,1.0,en].png index 0016e78cb8..06dcb0c6f8 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a17d073bf7c3e993c81e51e234c4f885d2264f6de8bf54a93306f14ee5a02a66 -size 33354 +oid sha256:7465cc5ca6b9f49ab4ac1a529fc2ee21547afb85a231d74491df6b2a2781bd18 +size 33075 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_2,NEXUS_5,1.0,en].png index c86faebfbc..13b74220ae 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea52227dcdf0806e292971e1b7332db28964eadd242882a80b6486d691e9c2c9 -size 35219 +oid sha256:93d4eb6dce460237fe6650e965b901c28b020057907867ac9f73665032a85f88 +size 35427 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_3,NEXUS_5,1.0,en].png index a130ba4351..1253286deb 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0163f939dfc3103eff7ded67b2fbd31dd050f6a4150245c9f0fbf3c7d13daf3e -size 34806 +oid sha256:c8a655fb4e64fa300c6169d332fa894b6bb215a9acd09c0fbe876c7430f3ae1d +size 34786 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_4,NEXUS_5,1.0,en].png index 5b02de2ffb..ba3922dabf 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ab7c7c6ae6951bf173f24fddb70e99feacd92286b088a6921900c6b7e881000a -size 42122 +oid sha256:4329c322042bc8489177886e37f6d3a876cbfee8a8a73e3aae6414a17e92ad67 +size 42330 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_5,NEXUS_5,1.0,en].png index 0d6bc21206..237efc2b36 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4063c0c9f763304aea54a73aaf95efc636d029cab45369b24a3bfc0b3bf3731d -size 42774 +oid sha256:92e67efa400e387c6eaab1933b763d01a711030ec582c9bb01d6b75f925db660 +size 42570 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_6,NEXUS_5,1.0,en].png index 0d6bc21206..237efc2b36 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4063c0c9f763304aea54a73aaf95efc636d029cab45369b24a3bfc0b3bf3731d -size 42774 +oid sha256:92e67efa400e387c6eaab1933b763d01a711030ec582c9bb01d6b75f925db660 +size 42570 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_7,NEXUS_5,1.0,en].png index 9f0e81e3c0..265ed3a00e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dabfea9a1b7604977444e8fedc977a61ab5576c6115aa97bffd77ad1d5c65038 -size 46198 +oid sha256:8359c8e69df44350b30b5aaf9ad99ace52fe378ba6db2cfd86a4deb35aa3abd1 +size 46347 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_8,NEXUS_5,1.0,en].png index 351feab0c8..6bcd031f28 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d1dfdb2a8cca036992e06a4d2a626c32d40c899e85bd5d5cf3aaba4272d339d8 -size 44244 +oid sha256:8659b897a22fd88915a8df3de3f0ba8d6775057f51b1c5cd1db83b0e58497576 +size 44394 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_9,NEXUS_5,1.0,en].png index 66ac910cef..211b17e145 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetailsDark_null_RoomDetailsDark--1_1_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fd7974c00bce66a8a1715e956c64aa578db303ff287aefb9dc5bb7906e4e27d5 -size 44406 +oid sha256:7a195980dff4ad4343eec99438a35ab655abd124346f5e4d66011c3c1736938e +size 44589 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_0,NEXUS_5,1.0,en].png index 947b00cb3d..b377d61fc3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:258605b56cbfb5ade4ce390b7e38a37b0602612d45a751b0032e4ba757d8d76f -size 45766 +oid sha256:29abf008345cd7c7150a64b17c6bd1c0af06c8d960c5e91a2cf0e4b1c0365954 +size 48791 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_1,NEXUS_5,1.0,en].png index 5d76493733..c359f319de 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c984e6333a0db11f1e52f246f019b927d97910dd35f5b79aae6d7c4f3094f913 -size 34667 +oid sha256:e6228ac8af567a9d4fc402d2b14ff65315f6cfca2d02407664b0db00df5fa68e +size 34363 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_2,NEXUS_5,1.0,en].png index a640c79a22..ffce2cd625 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:613b0bd1053c46b45fe6b58c754b03c69bab0a6cc2d90cd99278b4786f1253b0 -size 36681 +oid sha256:3987ef26780da018bedbe05b1af43c3a9c3f31963aff1eb17833a284118d29ec +size 36951 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_3,NEXUS_5,1.0,en].png index 400b589f07..65597d5fb9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:97416e171d87d7290924270c175b44a10c2b25fd3541156bf9e5258975f0c3a4 -size 35627 +oid sha256:c74c580b51aaa480a1ca15f2f33e3e6acb9e2a5581d5501ff67b6c4b56d92099 +size 35582 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_4,NEXUS_5,1.0,en].png index 14f86e1167..4e26b62c8b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:28a271552ea0e61954646c47bd8e87048c92cace7ca4176901c7b4d000bbc53f -size 43375 +oid sha256:d81c82c5d5f07bb57911488e18e2cf0867009233ad678557a8ead13e6827ba2b +size 43640 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_5,NEXUS_5,1.0,en].png index c4fddf0f15..91e6ec637d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7a4ddad1a6f2afad344e8e88fd297c91a5064206b2dbcb1130dd15b5fe22fbc9 -size 44086 +oid sha256:883e53d8ad2f6da6e3002274d5d808081e89242b43ec398d1366e986b7329d71 +size 43849 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_6,NEXUS_5,1.0,en].png index c4fddf0f15..91e6ec637d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7a4ddad1a6f2afad344e8e88fd297c91a5064206b2dbcb1130dd15b5fe22fbc9 -size 44086 +oid sha256:883e53d8ad2f6da6e3002274d5d808081e89242b43ec398d1366e986b7329d71 +size 43849 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_7,NEXUS_5,1.0,en].png index 49af7727ee..b708fb2592 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:36e579b569043783e8890a6cf5ffa1ae511c524c20f923765825ab9a9ccf9bb4 -size 47592 +oid sha256:bdfe3fb1c1594335fda27a62f2c1b4740815778248ba76923a2184937db02144 +size 47743 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_8,NEXUS_5,1.0,en].png index 3571ab69cd..ee4cb47b6f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b27ecb48f63242353d750c5fb2eb5cf2fcb3c11edd046cc9c03db57f3d26122 -size 45472 +oid sha256:d23cfed590306aec6eb92664c616e38f18e54274bb381e2e044e22916e8b1129 +size 45723 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_9,NEXUS_5,1.0,en].png index 693457317c..3f2e702024 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6fb2205cfbb68ae2e808fe62765311b10921a8bd579e23803484a492de36d028 -size 45645 +oid sha256:33e9d17227fda64a35cd885d029b70bab0088d5748cc5440c32d1cfa0a3bbf52 +size 45908 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_SelectedUserCannotRemove_null_SelectedUserCannotRemove-Day-7_8_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_SelectedUserCannotRemove_null_SelectedUserCannotRemove-Day-7_8_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d49e59cb7e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_SelectedUserCannotRemove_null_SelectedUserCannotRemove-Day-7_8_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d43c967fc2eca972c4c563afad5a3161de80424441c0853f20f27e297e421844 +size 8004 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_SelectedUserCannotRemove_null_SelectedUserCannotRemove-Night-7_9_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_SelectedUserCannotRemove_null_SelectedUserCannotRemove-Night-7_9_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f6ae863552 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_SelectedUserCannotRemove_null_SelectedUserCannotRemove-Night-7_9_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:016da4bfe577460531337a6c301aa4490dcbeb22f6b3b793e0cbdd9393189403 +size 8234 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_SelectedUsersList_null_SelectedUsersList-Day-7_8_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_SelectedUsersList_null_SelectedUsersList-Day-8_9_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_SelectedUsersList_null_SelectedUsersList-Day-7_8_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_SelectedUsersList_null_SelectedUsersList-Day-8_9_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_SelectedUsersList_null_SelectedUsersList-Night-7_9_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_SelectedUsersList_null_SelectedUsersList-Night-8_10_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_SelectedUsersList_null_SelectedUsersList-Night-7_9_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_SelectedUsersList_null_SelectedUsersList-Night-8_10_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_UnsavedAvatar_null_UnsavedAvatar-Day-8_9_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_UnsavedAvatar_null_UnsavedAvatar-Day-9_10_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_UnsavedAvatar_null_UnsavedAvatar-Day-8_9_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_UnsavedAvatar_null_UnsavedAvatar-Day-9_10_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_UnsavedAvatar_null_UnsavedAvatar-Night-8_10_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_UnsavedAvatar_null_UnsavedAvatar-Night-9_11_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_UnsavedAvatar_null_UnsavedAvatar-Night-8_10_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.matrix.ui.components_UnsavedAvatar_null_UnsavedAvatar-Night-9_11_null,NEXUS_5,1.0,en].png diff --git a/tools/localazy/config.json b/tools/localazy/config.json index f87f071ba4..96f39967a5 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -120,7 +120,9 @@ "screen_notification_settings_edit_failed_updating_default_mode", "screen_polls_history_title", "screen_notification_settings_mentions_only_disclaimer", - "screen_start_chat_error_starting_chat" + "screen_start_chat_error_starting_chat", + "screen_room_change_.*", + "screen_room_roles_.*" ] }, {