diff --git a/changelog.d/2258.feature b/changelog.d/2258.feature new file mode 100644 index 0000000000..32ee69f799 --- /dev/null +++ b/changelog.d/2258.feature @@ -0,0 +1 @@ +Room member moderation: kick, ban and unban users from a room. diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListEvents.kt index 43716660eb..9b36021c45 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListEvents.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListEvents.kt @@ -16,7 +16,10 @@ package io.element.android.features.roomdetails.impl.members +import io.element.android.libraries.matrix.api.room.RoomMember + sealed interface RoomMemberListEvents { data class UpdateSearchQuery(val query: String) : RoomMemberListEvents data class OnSearchActiveChanged(val active: Boolean) : RoomMemberListEvents + data class RoomMemberSelected(val roomMember: RoomMember) : RoomMemberListEvents } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt index 8148caaa5b..84580528b5 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt @@ -35,15 +35,16 @@ import io.element.android.services.analytics.api.AnalyticsService class RoomMemberListNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val presenter: RoomMemberListPresenter, + presenterFactory: RoomMemberListPresenter.Factory, private val analyticsService: AnalyticsService, -) : Node(buildContext, plugins = plugins) { +) : Node(buildContext, plugins = plugins), RoomMemberListNavigator { interface Callback : Plugin { fun openRoomMemberDetails(roomMemberId: UserId) fun openInviteMembers() } private val callbacks = plugins() + private val presenter = presenterFactory.create(this) init { lifecycle.subscribe( @@ -53,27 +54,35 @@ class RoomMemberListNode @AssistedInject constructor( ) } - private fun openRoomMemberDetails(roomMemberId: UserId) { + override fun openRoomMemberDetails(roomMemberId: UserId) { callbacks.forEach { it.openRoomMemberDetails(roomMemberId) } } - private fun openInviteMembers() { + override fun openInviteMembers() { callbacks.forEach { it.openInviteMembers() } } + override fun exitRoomMemberList() { + navigateUp() + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() RoomMemberListView( state = state, modifier = modifier, - onBackPressed = { navigateUp() }, - onMemberSelected = this::openRoomMemberDetails, - onInvitePressed = this::openInviteMembers, + navigator = this, ) } } + +interface RoomMemberListNavigator { + fun exitRoomMemberList() {} + fun openRoomMemberDetails(roomMemberId: UserId) {} + fun openInviteMembers() {} +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt index 04c1b50912..4c9cd327b2 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt @@ -23,32 +23,45 @@ 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.moderation.RoomMembersModerationEvents +import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationPresenter import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.RoomMembershipState -import io.element.android.libraries.matrix.api.room.powerlevels.canBan import io.element.android.libraries.matrix.api.room.powerlevels.canInvite import io.element.android.libraries.matrix.api.room.roomMembers import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import javax.inject.Inject -class RoomMemberListPresenter @Inject constructor( +class RoomMemberListPresenter @AssistedInject constructor( private val room: MatrixRoom, private val roomMemberListDataSource: RoomMemberListDataSource, private val coroutineDispatchers: CoroutineDispatchers, + private val featureFlagService: FeatureFlagService, + private val roomMembersModerationPresenter: RoomMembersModerationPresenter, + @Assisted private val navigator: RoomMemberListNavigator, ) : Presenter { + @AssistedFactory + interface Factory { + fun create(navigator: RoomMemberListNavigator): RoomMemberListPresenter + } + @Composable override fun present(): RoomMemberListState { + val coroutineScope = rememberCoroutineScope() var roomMembers by remember { mutableStateOf>(AsyncData.Loading()) } var searchQuery by rememberSaveable { mutableStateOf("") } var searchResults by remember { @@ -61,18 +74,14 @@ class RoomMemberListPresenter @Inject constructor( value = room.canInvite().getOrElse { false } } - val canDisplayBannedUsers by produceState(initialValue = false) { - val roomIsNotDmAndUserCanBan = !room.isDm && room.canBan().getOrElse { false } - if (roomIsNotDmAndUserCanBan) { - room.membersStateFlow - .onEach { members -> - val hasBannedUsers = members.roomMembers()?.any { it.membership == RoomMembershipState.BAN }.orFalse() - value = hasBannedUsers - } - .collect() - } else { - value = false - } + val isRoomModerationEnabled by produceState(initialValue = false) { + value = featureFlagService.isFeatureEnabled(FeatureFlags.RoomModeration) + } + + val roomModerationState = if (isRoomModerationEnabled) { + roomMembersModerationPresenter.present() + } else { + remember { roomMembersModerationPresenter.dummyState() } } LaunchedEffect(membersState) { @@ -116,19 +125,28 @@ class RoomMemberListPresenter @Inject constructor( } } + fun handleEvents(event: RoomMemberListEvents) { + when (event) { + is RoomMemberListEvents.OnSearchActiveChanged -> isSearchActive = event.active + is RoomMemberListEvents.UpdateSearchQuery -> searchQuery = event.query + is RoomMemberListEvents.RoomMemberSelected -> coroutineScope.launch { + if (roomMembersModerationPresenter.canDisplayModerationActions()) { + roomModerationState.eventSink(RoomMembersModerationEvents.SelectRoomMember(event.roomMember)) + } else { + navigator.openRoomMemberDetails(event.roomMember.userId) + } + } + } + } + return RoomMemberListState( roomMembers = roomMembers, searchQuery = searchQuery, searchResults = searchResults, isSearchActive = isSearchActive, canInvite = canInvite, - canDisplayBannedUsers = canDisplayBannedUsers, - eventSink = { event -> - when (event) { - is RoomMemberListEvents.OnSearchActiveChanged -> isSearchActive = event.active - is RoomMemberListEvents.UpdateSearchQuery -> searchQuery = event.query - } - }, + moderationState = roomModerationState, + eventSink = { handleEvents(it) }, ) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt index e9bcbffabb..368f099057 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt @@ -16,6 +16,7 @@ package io.element.android.features.roomdetails.impl.members +import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.api.room.RoomMember @@ -27,7 +28,7 @@ data class RoomMemberListState( val searchResults: SearchBarResultState, val isSearchActive: Boolean, val canInvite: Boolean, - val canDisplayBannedUsers: Boolean, + val moderationState: RoomMembersModerationState, val eventSink: (RoomMemberListEvents) -> Unit, ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt index 48c4559697..66f6933cef 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt @@ -17,6 +17,8 @@ package io.element.android.features.roomdetails.impl.members import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationState +import io.element.android.features.roomdetails.impl.members.moderation.aRoomMembersModerationState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.api.core.UserId @@ -57,30 +59,20 @@ internal class RoomMemberListStateProvider : PreviewParameterProvider = AsyncData.Uninitialized, searchResults: SearchBarResultState = SearchBarResultState.Initial(), - canDisplayBannedUsers: Boolean = false, + moderationState: RoomMembersModerationState = aRoomMembersModerationState(), ) = RoomMemberListState( roomMembers = roomMembers, searchQuery = "", searchResults = searchResults, isSearchActive = false, canInvite = false, - canDisplayBannedUsers = canDisplayBannedUsers, + moderationState = moderationState, eventSink = {} ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt index 2bfdbecc74..d7de4da573 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt @@ -34,6 +34,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -46,6 +47,8 @@ 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.moderation.RoomMembersModerationView +import io.element.android.features.roomdetails.impl.members.moderation.aRoomMembersModerationState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.button.BackButton @@ -76,14 +79,12 @@ private enum class SelectedSection { @Composable fun RoomMemberListView( state: RoomMemberListState, - onBackPressed: () -> Unit, - onInvitePressed: () -> Unit, - onMemberSelected: (UserId) -> Unit, + navigator: RoomMemberListNavigator, modifier: Modifier = Modifier, initialSelectedSectionIndex: Int = 0, ) { fun onUserSelected(roomMember: RoomMember) { - onMemberSelected(roomMember.userId) + state.eventSink(RoomMemberListEvents.RoomMemberSelected(roomMember)) } Scaffold( @@ -92,13 +93,18 @@ fun RoomMemberListView( if (!state.isSearchActive) { RoomMemberListTopBar( canInvite = state.canInvite, - onBackPressed = onBackPressed, - onInvitePressed = onInvitePressed, + onBackPressed = navigator::exitRoomMemberList, + onInvitePressed = navigator::openInviteMembers, ) } } ) { padding -> var selectedSection by remember { mutableStateOf(SelectedSection.entries[initialSelectedSectionIndex]) } + if (!state.moderationState.canDisplayBannedUsers && selectedSection == SelectedSection.BANNED) { + SideEffect { + selectedSection = SelectedSection.MEMBERS + } + } Column( modifier = Modifier .fillMaxWidth() @@ -123,7 +129,7 @@ fun RoomMemberListView( RoomMemberList( roomMembers = state.roomMembers.data, showMembersCount = true, - canDisplayBannedUsersControls = state.canDisplayBannedUsers, + canDisplayBannedUsersControls = state.moderationState.canDisplayBannedUsers, selectedSection = selectedSection, onSelectedSectionChanged = { selectedSection = it }, onUserSelected = ::onUserSelected, @@ -136,6 +142,11 @@ fun RoomMemberListView( } } } + + RoomMembersModerationView( + state = state.moderationState, + onDisplayMemberProfile = navigator::openRoomMemberDetails + ) } @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @@ -328,9 +339,7 @@ private fun RoomMemberSearchBar( internal fun RoomMemberListPreview(@PreviewParameter(RoomMemberListStateProvider::class) state: RoomMemberListState) = ElementPreview { RoomMemberListView( state = state, - onBackPressed = {}, - onMemberSelected = {}, - onInvitePressed = {}, + navigator = object : RoomMemberListNavigator {}, ) } @@ -351,10 +360,8 @@ internal fun RoomMemberBannedListPreview() = ElementPreview { ), ) ), - canDisplayBannedUsers = true, + moderationState = aRoomMembersModerationState(canDisplayBannedUsers = true), ), - onBackPressed = {}, - onMemberSelected = {}, - onInvitePressed = {}, + navigator = object : RoomMemberListNavigator {}, ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/DefaultRoomMembersModerationPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/DefaultRoomMembersModerationPresenter.kt new file mode 100644 index 0000000000..72a46c3463 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/DefaultRoomMembersModerationPresenter.kt @@ -0,0 +1,181 @@ +/* + * 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.members.moderation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +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.setValue +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.runUpdatingState +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.core.extensions.finally +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags +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.RoomMembershipState +import io.element.android.libraries.matrix.api.room.powerlevels.canBan +import io.element.android.libraries.matrix.api.room.powerlevels.canKick +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.launch +import javax.inject.Inject + +@ContributesBinding(RoomScope::class) +class DefaultRoomMembersModerationPresenter @Inject constructor( + private val room: MatrixRoom, + private val featureFlagService: FeatureFlagService, + private val dispatchers: CoroutineDispatchers, +) : RoomMembersModerationPresenter { + private var selectedMember by mutableStateOf(null) + + private suspend fun canBan() = room.canBan().getOrDefault(false) + private suspend fun canKick() = room.canKick().getOrDefault(false) + + override suspend fun canDisplayModerationActions(): Boolean { + val isRoomModerationEnabled = featureFlagService.isFeatureEnabled(FeatureFlags.RoomModeration) + val isDm = room.isDm && room.isEncrypted + return isRoomModerationEnabled && !isDm && (canBan() || canKick()) + } + + @Composable + override fun present(): RoomMembersModerationState { + val coroutineScope = rememberCoroutineScope() + var moderationActions by remember { mutableStateOf(persistentListOf()) } + + val kickUserAsyncAction = remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction) } + val banUserAsyncAction = remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction) } + val unbanUserAsyncAction = remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction) } + + val canDisplayBannedUsers by produceState(initialValue = false) { + value = featureFlagService.isFeatureEnabled(FeatureFlags.RoomModeration) && !room.isDm && canBan() + } + + fun handleEvent(event: RoomMembersModerationEvents) { + when (event) { + is RoomMembersModerationEvents.SelectRoomMember -> { + coroutineScope.launch { + selectedMember = event.roomMember + if (event.roomMember.membership == RoomMembershipState.BAN && canBan()) { + unbanUserAsyncAction.value = AsyncAction.Confirming + } else { + moderationActions = buildList { + add(ModerationAction.DisplayProfile(event.roomMember.userId)) + val currentUserMemberPowerLevel = room.userRole(room.sessionId).getOrDefault(RoomMember.Role.USER).powerLevel + if (currentUserMemberPowerLevel > event.roomMember.powerLevel) { + if (canKick()) { + add(ModerationAction.KickUser(event.roomMember.userId)) + } + if (canBan()) { + add(ModerationAction.BanUser(event.roomMember.userId)) + } + } + }.toPersistentList() + } + } + } + is RoomMembersModerationEvents.KickUser -> { + moderationActions = persistentListOf() + selectedMember?.let { + coroutineScope.kickUser(it.userId, kickUserAsyncAction) + } + } + is RoomMembersModerationEvents.BanUser -> { + if (banUserAsyncAction.value.isConfirming()) { + moderationActions = persistentListOf() + selectedMember?.let { + coroutineScope.banUser(it.userId, banUserAsyncAction) + } + } else { + banUserAsyncAction.value = AsyncAction.Confirming + } + } + is RoomMembersModerationEvents.UnbanUser -> { + if (unbanUserAsyncAction.value.isConfirming()) { + moderationActions = persistentListOf() + selectedMember?.let { + coroutineScope.unbanUser(it.userId, unbanUserAsyncAction) + } + } else { + unbanUserAsyncAction.value = AsyncAction.Confirming + } + } + is RoomMembersModerationEvents.Reset -> { + selectedMember = null + moderationActions = persistentListOf() + kickUserAsyncAction.value = AsyncAction.Uninitialized + banUserAsyncAction.value = AsyncAction.Uninitialized + unbanUserAsyncAction.value = AsyncAction.Uninitialized + } + } + } + + return RoomMembersModerationState( + selectedRoomMember = selectedMember, + actions = moderationActions, + kickUserAsyncAction = kickUserAsyncAction.value, + banUserAsyncAction = banUserAsyncAction.value, + unbanUserAsyncAction = unbanUserAsyncAction.value, + canDisplayBannedUsers = canDisplayBannedUsers, + eventSink = { handleEvent(it) }, + ) + } + + private fun CoroutineScope.kickUser( + userId: UserId, + kickUserAction: MutableState>, + ) = runActionAndWaitForMembershipChange(kickUserAction) { + room.kickUser(userId).finally { selectedMember = null } + } + + private fun CoroutineScope.banUser( + userId: UserId, + banUserAction: MutableState>, + ) = runActionAndWaitForMembershipChange(banUserAction) { + room.banUser(userId).finally { selectedMember = null } + } + + private fun CoroutineScope.unbanUser( + userId: UserId, + unbanUserAction: MutableState>, + ) = runActionAndWaitForMembershipChange(unbanUserAction) { + room.unbanUser(userId).finally { selectedMember = null } + } + + private fun CoroutineScope.runActionAndWaitForMembershipChange(action: MutableState>, block: suspend () -> Result) { + launch(dispatchers.io) { + action.runUpdatingState { + val result = block() + if (result.isSuccess) { + room.membersStateFlow.drop(1).take(1) + } + result + } + } + } +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationEvents.kt new file mode 100644 index 0000000000..ab195e0379 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationEvents.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.features.roomdetails.impl.members.moderation + +import io.element.android.libraries.matrix.api.room.RoomMember + +sealed interface RoomMembersModerationEvents { + data class SelectRoomMember(val roomMember: RoomMember) : RoomMembersModerationEvents + data object KickUser : RoomMembersModerationEvents + data object BanUser : RoomMembersModerationEvents + data object UnbanUser : RoomMembersModerationEvents + data object Reset : RoomMembersModerationEvents +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenter.kt new file mode 100644 index 0000000000..003bb758c7 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenter.kt @@ -0,0 +1,35 @@ +/* + * 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.members.moderation + +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.Presenter +import kotlinx.collections.immutable.persistentListOf + +interface RoomMembersModerationPresenter : Presenter { + suspend fun canDisplayModerationActions(): Boolean + + fun dummyState() = RoomMembersModerationState( + selectedRoomMember = null, + actions = persistentListOf(), + kickUserAsyncAction = AsyncAction.Uninitialized, + banUserAsyncAction = AsyncAction.Uninitialized, + unbanUserAsyncAction = AsyncAction.Uninitialized, + canDisplayBannedUsers = false, + eventSink = {} + ) +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationState.kt new file mode 100644 index 0000000000..f31205bdcf --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationState.kt @@ -0,0 +1,38 @@ +/* + * 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.members.moderation + +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.RoomMember +import kotlinx.collections.immutable.ImmutableList + +data class RoomMembersModerationState( + val selectedRoomMember: RoomMember?, + val actions: ImmutableList, + val kickUserAsyncAction: AsyncAction, + val banUserAsyncAction: AsyncAction, + val unbanUserAsyncAction: AsyncAction, + val canDisplayBannedUsers: Boolean, + val eventSink: (RoomMembersModerationEvents) -> Unit, +) + +sealed interface ModerationAction { + data class DisplayProfile(val userId: UserId) : ModerationAction + data class KickUser(val userId: UserId) : ModerationAction + data class BanUser(val userId: UserId) : ModerationAction +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationStatePreviewProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationStatePreviewProvider.kt new file mode 100644 index 0000000000..dcbdd1c986 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationStatePreviewProvider.kt @@ -0,0 +1,99 @@ +/* + * 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.members.moderation + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.roomdetails.impl.members.anAlice +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.room.RoomMember +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList + +class RoomMembersModerationStatePreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aRoomMembersModerationState( + selectedRoomMember = anAlice(), + actions = persistentListOf( + ModerationAction.DisplayProfile(anAlice().userId), + ), + ), + aRoomMembersModerationState( + selectedRoomMember = anAlice(), + actions = persistentListOf( + ModerationAction.DisplayProfile(anAlice().userId), + ModerationAction.KickUser(userId = anAlice().userId), + ), + ), + aRoomMembersModerationState( + selectedRoomMember = anAlice(), + actions = persistentListOf( + ModerationAction.DisplayProfile(anAlice().userId), + ModerationAction.KickUser(userId = anAlice().userId), + ModerationAction.BanUser(userId = anAlice().userId), + ), + ), + aRoomMembersModerationState( + selectedRoomMember = anAlice(), + kickUserAsyncAction = AsyncAction.Loading, + ), + aRoomMembersModerationState( + selectedRoomMember = anAlice(), + banUserAsyncAction = AsyncAction.Loading, + ), + aRoomMembersModerationState( + selectedRoomMember = anAlice(), + unbanUserAsyncAction = AsyncAction.Loading, + ), + aRoomMembersModerationState( + kickUserAsyncAction = AsyncAction.Failure(Exception("Failed to kick user")), + banUserAsyncAction = AsyncAction.Failure(Exception("Failed to ban user")), + unbanUserAsyncAction = AsyncAction.Failure(Exception("Failed to unban user")), + ), + aRoomMembersModerationState( + selectedRoomMember = anAlice(), + banUserAsyncAction = AsyncAction.Confirming, + ), + aRoomMembersModerationState( + selectedRoomMember = anAlice(), + unbanUserAsyncAction = AsyncAction.Confirming, + ), + aRoomMembersModerationState( + kickUserAsyncAction = AsyncAction.Success(Unit), + banUserAsyncAction = AsyncAction.Success(Unit), + unbanUserAsyncAction = AsyncAction.Success(Unit), + ), + ) +} + +fun aRoomMembersModerationState( + selectedRoomMember: RoomMember? = null, + actions: List = emptyList(), + kickUserAsyncAction: AsyncAction = AsyncAction.Uninitialized, + banUserAsyncAction: AsyncAction = AsyncAction.Uninitialized, + unbanUserAsyncAction: AsyncAction = AsyncAction.Uninitialized, + canDisplayBannedUsers: Boolean = false, + eventSink: (RoomMembersModerationEvents) -> Unit = {}, +) = RoomMembersModerationState( + selectedRoomMember = selectedRoomMember, + actions = actions.toPersistentList(), + kickUserAsyncAction = kickUserAsyncAction, + banUserAsyncAction = banUserAsyncAction, + unbanUserAsyncAction = unbanUserAsyncAction, + canDisplayBannedUsers = canDisplayBannedUsers, + eventSink = eventSink, +) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationView.kt new file mode 100644 index 0000000000..8610269ead --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationView.kt @@ -0,0 +1,318 @@ +/* + * 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.members.moderation + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +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.LaunchedEffect +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +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.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.avatar.Avatar +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.designsystem.components.list.ListItemContent +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.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.ModalBottomSheet +import io.element.android.libraries.designsystem.theme.components.Text +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.room.getBestName +import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.launch +import timber.log.Timber + +@Composable +fun RoomMembersModerationView( + state: RoomMembersModerationState, + onDisplayMemberProfile: (UserId) -> Unit, + modifier: Modifier = Modifier, +) { + Box(modifier = modifier) { + if (state.actions.isNotEmpty()) { + RoomMemberActionsBottomSheet( + roomMember = state.selectedRoomMember, + actions = state.actions, + onActionSelected = { action -> + when (action) { + is ModerationAction.DisplayProfile -> { + onDisplayMemberProfile(action.userId) + } + is ModerationAction.KickUser -> { + state.eventSink(RoomMembersModerationEvents.KickUser) + } + is ModerationAction.BanUser -> { + state.eventSink(RoomMembersModerationEvents.BanUser) + } + } + }, + onDismiss = { state.eventSink(RoomMembersModerationEvents.Reset) }, + ) + } + + val asyncIndicatorState = rememberAsyncIndicatorState() + AsyncIndicatorHost(modifier = Modifier.statusBarsPadding(), state = asyncIndicatorState) + + when (val action = state.kickUserAsyncAction) { + is AsyncAction.Loading -> { + LaunchedEffect(action) { + val userDisplayName = state.selectedRoomMember?.getBestName().orEmpty() + asyncIndicatorState.enqueue { + AsyncIndicator.Loading(text = stringResource(R.string.screen_room_member_list_removing_user, userDisplayName)) + } + } + } + is AsyncAction.Failure -> { + Timber.e(action.error, "Failed to kick user.") + LaunchedEffect(action) { + asyncIndicatorState.enqueue(AsyncIndicator.DURATION_SHORT) { + AsyncIndicator.Failure( + text = stringResource(CommonStrings.common_failed), + ) + } + } + } + is AsyncAction.Success -> { + LaunchedEffect(action) { asyncIndicatorState.clear() } + } + else -> Unit + } + + when (val action = state.banUserAsyncAction) { + is AsyncAction.Confirming -> { + ConfirmationDialog( + title = stringResource(R.string.screen_room_member_list_ban_member_confirmation_title), + content = stringResource(R.string.screen_room_member_list_ban_member_confirmation_description), + submitText = stringResource(R.string.screen_room_member_list_ban_member_confirmation_action), + onSubmitClicked = { state.selectedRoomMember?.userId?.let { state.eventSink(RoomMembersModerationEvents.BanUser) } }, + onDismiss = { state.eventSink(RoomMembersModerationEvents.Reset) } + ) + } + is AsyncAction.Loading -> { + LaunchedEffect(action) { + val userDisplayName = state.selectedRoomMember?.getBestName().orEmpty() + asyncIndicatorState.enqueue { + AsyncIndicator.Loading(text = stringResource(R.string.screen_room_member_list_banning_user, userDisplayName)) + } + } + } + is AsyncAction.Failure -> { + Timber.e(action.error, "Failed to ban user.") + LaunchedEffect(action) { + asyncIndicatorState.enqueue(AsyncIndicator.DURATION_SHORT) { + AsyncIndicator.Failure( + text = stringResource(CommonStrings.common_failed), + ) + } + } + } + is AsyncAction.Success -> { + LaunchedEffect(action) { asyncIndicatorState.clear() } + } + else -> Unit + } + + when (val action = state.unbanUserAsyncAction) { + is AsyncAction.Confirming -> { + state.selectedRoomMember?.let { + ConfirmationDialog( + title = stringResource(R.string.screen_room_member_list_manage_member_unban_title), + content = stringResource(R.string.screen_room_member_list_manage_member_unban_message), + submitText = stringResource(R.string.screen_room_member_list_manage_member_unban_action), + onSubmitClicked = { state.eventSink(RoomMembersModerationEvents.UnbanUser) }, + onDismiss = { state.eventSink(RoomMembersModerationEvents.Reset) }, + ) + } + } + is AsyncAction.Loading -> { + LaunchedEffect(action) { + val userDisplayName = state.selectedRoomMember?.getBestName().orEmpty() + asyncIndicatorState.enqueue { + AsyncIndicator.Loading(text = stringResource(R.string.screen_room_member_list_unbanning_user, userDisplayName)) + } + } + } + is AsyncAction.Failure -> { + Timber.e(action.error, "Failed to unban user.") + LaunchedEffect(action) { + asyncIndicatorState.enqueue(AsyncIndicator.DURATION_SHORT) { + AsyncIndicator.Failure( + text = stringResource(CommonStrings.common_failed), + ) + } + } + } + is AsyncAction.Success -> { + LaunchedEffect(action) { asyncIndicatorState.clear() } + } + else -> Unit + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun RoomMemberActionsBottomSheet( + roomMember: RoomMember?, + actions: ImmutableList, + onActionSelected: (ModerationAction) -> Unit, + onDismiss: () -> Unit, +) { + val coroutineScope = rememberCoroutineScope() + if (roomMember != null && actions.isNotEmpty()) { + val bottomSheetState = if (LocalInspectionMode.current) { + sheetStateForPreview() + } else { + rememberModalBottomSheetState(skipPartiallyExpanded = true) + } + ModalBottomSheet( + modifier = Modifier.systemBarsPadding(), + sheetState = bottomSheetState, + onDismissRequest = { + coroutineScope.launch { + bottomSheetState.hide() + onDismiss() + } + }, + ) { + Column( + modifier = Modifier.padding(vertical = 16.dp) + ) { + Avatar( + avatarData = AvatarData( + id = roomMember.userId.value, + name = roomMember.displayName, + url = roomMember.avatarUrl, + size = AvatarSize.RoomListManageUser, + ), + modifier = Modifier + .padding(bottom = 28.dp) + .align(Alignment.CenterHorizontally) + ) + roomMember.displayName?.let { + Text( + text = it, + style = ElementTheme.typography.fontHeadingLgBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center, + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 8.dp) + .fillMaxWidth() + ) + } + Text( + text = roomMember.userId.toString(), + style = ElementTheme.typography.fontBodyLgRegular, + color = ElementTheme.colors.textSecondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center, + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth() + ) + Spacer(modifier = Modifier.height(32.dp)) + + for (action in actions) { + when (action) { + is ModerationAction.DisplayProfile -> { + ListItem( + headlineContent = { Text(stringResource(R.string.screen_room_member_list_manage_member_user_info)) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Info())), + onClick = { + coroutineScope.launch { + onActionSelected(action) + bottomSheetState.hide() + } + } + ) + } + is ModerationAction.KickUser -> { + ListItem( + headlineContent = { Text(stringResource(R.string.screen_room_member_list_manage_member_remove)) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Block())), + onClick = { + coroutineScope.launch { + bottomSheetState.hide() + onActionSelected(action) + } + } + ) + } + is ModerationAction.BanUser -> { + ListItem( + headlineContent = { Text(stringResource(R.string.screen_room_member_list_manage_member_remove_confirmation_ban)) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Block())), + style = ListItemStyle.Destructive, + onClick = { + coroutineScope.launch { + bottomSheetState.hide() + onActionSelected(action) + } + } + ) + } + } + } + } + } + } +} + +@PreviewsDayNight +@Composable +internal fun RoomMembersModerationViewPreview(@PreviewParameter(RoomMembersModerationStatePreviewProvider::class) state: RoomMembersModerationState) { + ElementPreview { + Box(modifier = Modifier.fillMaxWidth().heightIn(min = 64.dp)) { + RoomMembersModerationView( + state = state, + onDisplayMemberProfile = {}, + ) + } + } +} diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt index 88e9c7df86..e70483356e 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt @@ -22,13 +22,21 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.roomdetails.impl.members.RoomMemberListDataSource import io.element.android.features.roomdetails.impl.members.RoomMemberListEvents +import io.element.android.features.roomdetails.impl.members.RoomMemberListNavigator import io.element.android.features.roomdetails.impl.members.RoomMemberListPresenter import io.element.android.features.roomdetails.impl.members.aRoomMemberList import io.element.android.features.roomdetails.impl.members.aVictor import io.element.android.features.roomdetails.impl.members.aWalter +import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationEvents +import io.element.android.features.roomdetails.impl.members.moderation.aRoomMembersModerationState +import io.element.android.features.roomdetails.members.moderation.FakeRoomMembersModerationPresenter import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService +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.MatrixRoomMembersState import io.element.android.libraries.matrix.test.room.FakeMatrixRoom @@ -167,6 +175,51 @@ class RoomMemberListPresenterTests { assertThat(loadedState.canInvite).isFalse() } } + + @Test + fun `present - RoomMemberSelected by default opens the room member details through the navigator`() = runTest { + val navigator = FakeRoomMemberListNavigator() + val moderationPresenter = FakeRoomMembersModerationPresenter(canDisplayModerationActions = false) + val presenter = createPresenter(moderationPresenter = moderationPresenter, navigator = navigator) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + awaitItem().eventSink(RoomMemberListEvents.RoomMemberSelected(aVictor())) + assertThat(navigator.openRoomMemberDetailsCallCount).isEqualTo(1) + } + } + + @Test + fun `present - RoomMemberSelected will open the moderation options if the current user can use them`() = runTest { + val navigator = FakeRoomMemberListNavigator() + var selectRoomMemberCallCounts = 0 + val capturingState = aRoomMembersModerationState(eventSink = { event -> + if (event is RoomMembersModerationEvents.SelectRoomMember) { + selectRoomMemberCallCounts++ + } + }) + val moderationPresenter = FakeRoomMembersModerationPresenter(canDisplayModerationActions = true).apply { + givenState(capturingState) + } + val presenter = createPresenter(moderationPresenter = moderationPresenter, navigator = navigator) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + awaitItem().eventSink(RoomMemberListEvents.RoomMemberSelected(aVictor())) + assertThat(selectRoomMemberCallCounts).isEqualTo(1) + } + } +} + +private class FakeRoomMemberListNavigator : RoomMemberListNavigator { + var openRoomMemberDetailsCallCount = 0 + private set + + override fun openRoomMemberDetails(userId: UserId) { + openRoomMemberDetailsCallCount++ + } } @ExperimentalCoroutinesApi @@ -182,4 +235,14 @@ private fun TestScope.createPresenter( coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), matrixRoom: MatrixRoom = FakeMatrixRoom(), roomMemberListDataSource: RoomMemberListDataSource = createDataSource(coroutineDispatchers = coroutineDispatchers), -) = RoomMemberListPresenter(matrixRoom, roomMemberListDataSource, coroutineDispatchers) + featureFlagService: FeatureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.RoomModeration.key to true)), + moderationPresenter: FakeRoomMembersModerationPresenter = FakeRoomMembersModerationPresenter(), + navigator: RoomMemberListNavigator = object : RoomMemberListNavigator { } +) = RoomMemberListPresenter( + room = matrixRoom, + roomMemberListDataSource = roomMemberListDataSource, + coroutineDispatchers = coroutineDispatchers, + featureFlagService = featureFlagService, + roomMembersModerationPresenter = moderationPresenter, + navigator = navigator +) diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/DefaultRoomMembersModerationPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/DefaultRoomMembersModerationPresenterTests.kt new file mode 100644 index 0000000000..73ad237923 --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/DefaultRoomMembersModerationPresenterTests.kt @@ -0,0 +1,313 @@ +/* + * 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.members.moderation + +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.aVictor +import io.element.android.features.roomdetails.impl.members.moderation.DefaultRoomMembersModerationPresenter +import io.element.android.features.roomdetails.impl.members.moderation.ModerationAction +import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationEvents +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService +import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState +import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.RoomMembershipState +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.persistentListOf +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DefaultRoomMembersModerationPresenterTests { + @Test + fun `canDisplayModerationActions - when feature flag is disabled returns false`() = runTest { + val featureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.RoomModeration.key to false)) + val presenter = createDefaultRoomMembersModerationPresenter(featureFlagService = featureFlagService) + assertThat(presenter.canDisplayModerationActions()).isFalse() + } + + @Test + fun `canDisplayModerationActions - when room is DM is false`() = runTest { + val room = FakeMatrixRoom(isDirect = true, isPublic = true, isOneToOne = true).apply { + givenRoomInfo(aRoomInfo(isDirect = true, isPublic = false, activeMembersCount = 2)) + } + val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room) + assertThat(presenter.canDisplayModerationActions()).isFalse() + } + + @Test + fun `canDisplayModerationActions - when user can kick other users, FF is enabled and room is not a DM returns true`() = runTest { + val room = FakeMatrixRoom(isDirect = false, isOneToOne = false).apply { + givenCanKickResult(Result.success(true)) + } + val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room) + assertThat(presenter.canDisplayModerationActions()).isTrue() + } + + @Test + fun `canDisplayModerationActions - when user can ban other users, FF is enabled and room is not a DM returns true`() = runTest { + val room = FakeMatrixRoom(isDirect = false, isOneToOne = false).apply { + givenCanBanResult(Result.success(true)) + } + val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room) + assertThat(presenter.canDisplayModerationActions()).isTrue() + } + + @Test + fun `present - SelectRoomMember when the current user has permissions displays member actions`() = runTest { + val room = FakeMatrixRoom().apply { + givenCanKickResult(Result.success(true)) + givenCanBanResult(Result.success(true)) + givenUserRoleResult(Result.success(RoomMember.Role.ADMIN)) + } + val selectedMember = aVictor() + val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember)) + with(awaitItem()) { + assertThat(this.selectedRoomMember).isNotNull() + assertThat(this.selectedRoomMember?.userId).isEqualTo(selectedMember.userId) + assertThat(actions).containsExactly( + ModerationAction.DisplayProfile(selectedMember.userId), + ModerationAction.KickUser(selectedMember.userId), + ModerationAction.BanUser(selectedMember.userId) + ) + } + } + } + + @Test + fun `present - SelectRoomMember displays only view profile if selected member has same power level as the current user`() = runTest { + val room = FakeMatrixRoom(sessionId = A_USER_ID).apply { + givenCanKickResult(Result.success(true)) + givenCanBanResult(Result.success(true)) + givenUserRoleResult(Result.success(RoomMember.Role.ADMIN)) + } + val selectedMember = aRoomMember(A_USER_ID_2, powerLevel = 100L) + val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember)) + with(awaitItem()) { + assertThat(this.selectedRoomMember).isNotNull() + assertThat(this.selectedRoomMember?.userId).isEqualTo(selectedMember.userId) + assertThat(actions).containsExactly( + ModerationAction.DisplayProfile(selectedMember.userId), + ) + } + } + } + + @Test + fun `present - SelectRoomMember displays an unban confirmation dialog when the member is banned`() = runTest { + val selectedMember = aRoomMember(A_USER_ID_2, membership = RoomMembershipState.BAN) + val room = FakeMatrixRoom().apply { + givenCanKickResult(Result.success(true)) + givenCanBanResult(Result.success(true)) + givenUserRoleResult(Result.success(RoomMember.Role.ADMIN)) + } + val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember)) + with(awaitItem()) { + assertThat(selectedRoomMember).isNotNull() + assertThat(unbanUserAsyncAction).isEqualTo(AsyncAction.Confirming) + } + } + } + + @Test + fun `present - Kick removes the user`() = runTest { + val room = FakeMatrixRoom().apply { + givenCanKickResult(Result.success(true)) + givenCanBanResult(Result.success(true)) + givenUserRoleResult(Result.success(RoomMember.Role.ADMIN)) + } + val selectedMember = aVictor() + val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember)) + awaitItem().eventSink(RoomMembersModerationEvents.KickUser) + skipItems(1) + assertThat(awaitItem().actions).isEmpty() + assertThat(awaitItem().kickUserAsyncAction).isEqualTo(AsyncAction.Loading) + with(awaitItem()) { + assertThat(kickUserAsyncAction).isEqualTo(AsyncAction.Success(Unit)) + assertThat(selectedRoomMember).isNull() + } + } + } + + @Test + fun `present - BanUser requires confirmation and then bans the user`() = runTest { + val room = FakeMatrixRoom().apply { + givenCanKickResult(Result.success(true)) + givenCanBanResult(Result.success(true)) + givenUserRoleResult(Result.success(RoomMember.Role.ADMIN)) + } + val selectedMember = aVictor() + val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember)) + awaitItem().eventSink(RoomMembersModerationEvents.BanUser) + val confirmingState = awaitItem() + assertThat(confirmingState.banUserAsyncAction).isEqualTo(AsyncAction.Confirming) + + // Confirm + confirmingState.eventSink(RoomMembersModerationEvents.BanUser) + skipItems(1) + assertThat(awaitItem().actions).isEmpty() + assertThat(awaitItem().banUserAsyncAction).isEqualTo(AsyncAction.Loading) + with(awaitItem()) { + assertThat(banUserAsyncAction).isEqualTo(AsyncAction.Success(Unit)) + assertThat(selectedRoomMember).isNull() + } + } + } + + @Test + fun `present - UnbanUser requires confirmation and then unbans the user`() = runTest { + val selectedMember = aRoomMember(A_USER_ID_2, membership = RoomMembershipState.BAN) + val room = FakeMatrixRoom().apply { + givenCanKickResult(Result.success(true)) + givenCanBanResult(Result.success(true)) + givenRoomMembersState(MatrixRoomMembersState.Ready(persistentListOf(selectedMember))) + givenUserRoleResult(Result.success(RoomMember.Role.ADMIN)) + } + val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + // Displays confirmation dialog + awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember)) + // Confirms unban + awaitItem().eventSink(RoomMembersModerationEvents.UnbanUser) + assertThat(awaitItem().actions).isEmpty() + assertThat(awaitItem().unbanUserAsyncAction).isEqualTo(AsyncAction.Loading) + with(awaitItem()) { + assertThat(unbanUserAsyncAction).isEqualTo(AsyncAction.Success(Unit)) + assertThat(selectedRoomMember).isNull() + } + } + } + + @Test + fun `present - Reset removes the selected user and actions`() = runTest { + val room = FakeMatrixRoom().apply { + givenCanKickResult(Result.success(true)) + givenCanBanResult(Result.success(true)) + } + val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + // Displays confirmation dialog + awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(aVictor())) + // Reset state + awaitItem().eventSink(RoomMembersModerationEvents.Reset) + assertThat(awaitItem().selectedRoomMember).isNull() + assertThat(awaitItem().actions).isEmpty() + } + } + + @Test + fun `present - Reset resets any async actions`() = runTest { + val room = FakeMatrixRoom().apply { + givenCanKickResult(Result.success(true)) + givenCanBanResult(Result.success(true)) + givenKickUserResult(Result.failure(Throwable("Eek"))) + givenBanUserResult(Result.failure(Throwable("Eek"))) + givenUnbanUserResult(Result.failure(Throwable("Eek"))) + } + val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialItem = awaitItem() + // Kick user and fail + awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(aVictor())) + awaitItem().eventSink(RoomMembersModerationEvents.KickUser) + skipItems(2) + assertThat(awaitItem().kickUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java) + assertThat(awaitItem().kickUserAsyncAction).isInstanceOf(AsyncAction.Failure::class.java) + // Reset it + initialItem.eventSink(RoomMembersModerationEvents.Reset) + assertThat(awaitItem().kickUserAsyncAction).isEqualTo(AsyncAction.Uninitialized) + + // Ban user and fail + initialItem.eventSink(RoomMembersModerationEvents.SelectRoomMember(aVictor())) + awaitItem().eventSink(RoomMembersModerationEvents.BanUser) + awaitItem().eventSink(RoomMembersModerationEvents.BanUser) + skipItems(2) + assertThat(awaitItem().banUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java) + assertThat(awaitItem().banUserAsyncAction).isInstanceOf(AsyncAction.Failure::class.java) + // Reset it + initialItem.eventSink(RoomMembersModerationEvents.Reset) + assertThat(awaitItem().banUserAsyncAction).isEqualTo(AsyncAction.Uninitialized) + + // Unban user and fail + initialItem.eventSink(RoomMembersModerationEvents.SelectRoomMember(aVictor().copy(membership = RoomMembershipState.BAN))) + val confirmingState = awaitItem() + assertThat(confirmingState.unbanUserAsyncAction).isInstanceOf(AsyncAction.Confirming::class.java) + confirmingState.eventSink(RoomMembersModerationEvents.UnbanUser) + skipItems(1) + assertThat(awaitItem().unbanUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java) + assertThat(awaitItem().unbanUserAsyncAction).isInstanceOf(AsyncAction.Failure::class.java) + // Reset it + initialItem.eventSink(RoomMembersModerationEvents.Reset) + assertThat(awaitItem().unbanUserAsyncAction).isEqualTo(AsyncAction.Uninitialized) + } + } + + private fun TestScope.createDefaultRoomMembersModerationPresenter( + matrixRoom: FakeMatrixRoom = FakeMatrixRoom(), + featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.RoomModeration.key to true)), + dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), + ): DefaultRoomMembersModerationPresenter { + return DefaultRoomMembersModerationPresenter( + room = matrixRoom, + featureFlagService = featureFlagService, + dispatchers = dispatchers, + ) + } +} diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/FakeRoomMembersModerationPresenter.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/FakeRoomMembersModerationPresenter.kt new file mode 100644 index 0000000000..c5958f8622 --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/FakeRoomMembersModerationPresenter.kt @@ -0,0 +1,43 @@ +/* + * 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.members.moderation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationPresenter +import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationState + +class FakeRoomMembersModerationPresenter( + private val canDisplayModerationActions: Boolean = true, +) : RoomMembersModerationPresenter { + private var state by mutableStateOf(dummyState()) + + override suspend fun canDisplayModerationActions(): Boolean { + return canDisplayModerationActions + } + + @Composable + override fun present(): RoomMembersModerationState { + return state + } + + fun givenState(state: RoomMembersModerationState) { + this.state = state + } +} diff --git a/features/roomlist/impl/src/main/res/values/localazy.xml b/features/roomlist/impl/src/main/res/values/localazy.xml index 210ed2fb7f..5044fd61a5 100644 --- a/features/roomlist/impl/src/main/res/values/localazy.xml +++ b/features/roomlist/impl/src/main/res/values/localazy.xml @@ -20,7 +20,7 @@ For now, you can deselect filters in order to see your other chats" "You’re not in any room yet" "Unreads" "Congrats! -You don’t have any unread message!" +You don’t have any unread messages!" "Chats" "Mark as read" "Mark as unread" diff --git a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/AsyncAction.kt b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/AsyncAction.kt index 38de169db3..e69b1b9eaf 100644 --- a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/AsyncAction.kt +++ b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/AsyncAction.kt @@ -79,9 +79,9 @@ sealed interface AsyncAction { fun isUninitialized(): Boolean = this == Uninitialized - fun isConfirming(): Boolean = this is Confirming + fun isConfirming(): Boolean = this == Confirming - fun isLoading(): Boolean = this is Loading + fun isLoading(): Boolean = this == Loading fun isFailure(): Boolean = this is Failure diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/Result.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/Result.kt index f7d96aebf5..32a8706399 100644 --- a/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/Result.kt +++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/Result.kt @@ -47,3 +47,9 @@ inline fun Result.flatMapCatching(transform: (T) -> Result): Result onFailure = { Result.failure(it) } ) } + +inline fun Result.finally(block: (exception: Throwable?) -> Unit): Result { + onSuccess { block(null) } + onFailure(block) + return this +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt index 9a5209ebfb..0451cb5839 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt @@ -47,6 +47,7 @@ enum class AvatarSize(val dp: Dp) { InviteSender(16.dp), EditRoomDetails(70.dp), + RoomListManageUser(70.dp), NotificationsOptIn(32.dp), diff --git a/libraries/designsystem/src/test/kotlin/io/element/android/libraries/designsystem/component/async/AsyncIndicatorTests.kt b/libraries/designsystem/src/test/kotlin/io/element/android/libraries/designsystem/component/async/AsyncIndicatorTests.kt index fb6f70040b..d82441b964 100644 --- a/libraries/designsystem/src/test/kotlin/io/element/android/libraries/designsystem/component/async/AsyncIndicatorTests.kt +++ b/libraries/designsystem/src/test/kotlin/io/element/android/libraries/designsystem/component/async/AsyncIndicatorTests.kt @@ -193,7 +193,7 @@ class AsyncIndicatorTests { currentAnimationState = TransitionStateSnapshot(transitionState), ) }.test { - var firstItem: Any? = null + var firstItem: Any? skipItems(1) state.enqueue(composable = {}) state.enqueue(composable = {}) 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 efe3107b31..7d9601f710 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 @@ -140,6 +140,8 @@ interface MatrixRoom : Closeable { suspend fun canUserInvite(userId: UserId): Result + suspend fun canUserKick(userId: UserId): Result + suspend fun canUserBan(userId: UserId): Result suspend fun canUserRedactOwn(userId: UserId): Result @@ -177,6 +179,12 @@ interface MatrixRoom : Closeable { suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result + suspend fun kickUser(userId: UserId, reason: String? = null): Result + + suspend fun banUser(userId: UserId, reason: String? = null): Result + + suspend fun unbanUser(userId: UserId, reason: String? = null): Result + suspend fun setIsFavorite(isFavorite: Boolean): Result /** diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomPowerLevels.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomPowerLevels.kt index c879991bc0..2950788c68 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomPowerLevels.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomPowerLevels.kt @@ -25,6 +25,11 @@ import io.element.android.libraries.matrix.api.room.StateEventType */ suspend fun MatrixRoom.canInvite(): Result = canUserInvite(sessionId) +/** + * Shortcut for calling [MatrixRoom.canUserKick] with our own user. + */ +suspend fun MatrixRoom.canKick(): Result = canUserKick(sessionId) + /** * Shortcut for calling [MatrixRoom.canBanUser] with our own user. */ diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MatrixTimeline.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MatrixTimeline.kt index 9a423d07bb..25760afc45 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MatrixTimeline.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MatrixTimeline.kt @@ -39,6 +39,7 @@ interface MatrixTimeline : AutoCloseable { val paginationState: StateFlow val timelineItems: Flow> + val membershipChangeEventReceived: Flow suspend fun paginateBackwards(requestSize: Int): Result suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result 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 c3a543cf60..3621ba1fd5 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 @@ -70,6 +70,8 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -159,6 +161,12 @@ class RustMatrixRoom( override val syncUpdateFlow: StateFlow = _syncUpdateFlow.asStateFlow() + init { + timeline.membershipChangeEventReceived + .onEach { roomMemberListFetcher.fetchRoomMembers() } + .launchIn(roomCoroutineScope) + } + override suspend fun subscribeToSync() = roomSyncSubscriber.subscribe(roomId) override suspend fun unsubscribeFromSync() = roomSyncSubscriber.unsubscribe(roomId) @@ -340,6 +348,12 @@ class RustMatrixRoom( } } + override suspend fun canUserKick(userId: UserId): Result { + return runCatching { + innerRoom.canUserKick(userId.value) + } + } + override suspend fun canUserBan(userId: UserId): Result { return runCatching { innerRoom.canUserBan(userId.value) @@ -469,6 +483,24 @@ class RustMatrixRoom( } } + override suspend fun kickUser(userId: UserId, reason: String?): Result = withContext(roomDispatcher) { + runCatching { + innerRoom.kickUser(userId.value, reason) + } + } + + override suspend fun banUser(userId: UserId, reason: String?): Result = withContext(roomDispatcher) { + runCatching { + innerRoom.banUser(userId.value, reason) + } + } + + override suspend fun unbanUser(userId: UserId, reason: String?): Result = withContext(roomDispatcher) { + runCatching { + innerRoom.unbanUser(userId.value, reason) + } + } + override suspend fun setIsFavorite(isFavorite: Boolean): Result = withContext(roomDispatcher) { runCatching { innerRoom.setIsFavourite(isFavorite, null) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberListFetcher.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberListFetcher.kt index a4e58f3cbe..e9b420273e 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberListFetcher.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberListFetcher.kt @@ -65,7 +65,7 @@ internal class RoomMemberListFetcher( if (_membersFlow.value !is MatrixRoomMembersState.Ready) { fetchCachedRoomMembers() } else { - Timber.i("No need to load cached members found for room $roomId") + Timber.i("Cached members not found for $roomId") } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/AsyncMatrixTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/AsyncMatrixTimeline.kt index 2d6fbd253b..1ec7e007e9 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/AsyncMatrixTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/AsyncMatrixTimeline.kt @@ -27,6 +27,7 @@ import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.async import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.launchIn @@ -55,6 +56,8 @@ class AsyncMatrixTimeline( } private val closeSignal = CompletableDeferred() + override val membershipChangeEventReceived = MutableSharedFlow(extraBufferCapacity = 1) + init { coroutineScope.launch { val delegateTimeline = timeline.await() @@ -64,6 +67,9 @@ class AsyncMatrixTimeline( delegateTimeline.paginationState .onEach { _paginationState.value = it } .launchIn(this) + delegateTimeline.membershipChangeEventReceived + .onEach { membershipChangeEventReceived.emit(it) } + .launchIn(this) launch { withContext(NonCancellable) { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessor.kt index 602220c1c7..e75f7cd699 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessor.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessor.kt @@ -17,6 +17,9 @@ package io.element.android.libraries.matrix.impl.timeline import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -31,6 +34,9 @@ internal class MatrixTimelineDiffProcessor( ) { private val mutex = Mutex() + private val _membershipChangeEventReceived = MutableSharedFlow(extraBufferCapacity = 1) + val membershipChangeEventReceived: Flow = _membershipChangeEventReceived + suspend fun postItems(items: List) { updateTimelineItems { Timber.v("Update timeline items from postItems (with ${items.size} items) on ${Thread.currentThread()}") @@ -63,6 +69,11 @@ internal class MatrixTimelineDiffProcessor( } TimelineChange.PUSH_BACK -> { val item = diff.pushBack()?.asMatrixTimelineItem() ?: return + if (item is MatrixTimelineItem.Event && item.event.content is RoomMembershipContent) { + // TODO - This is a temporary solution to notify the room screen about membership changes + // Ideally, this should be implemented by the Rust SDK + _membershipChangeEventReceived.tryEmit(Unit) + } add(item) } TimelineChange.PUSH_FRONT -> { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt index 0f8da55a75..b507690a4a 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt @@ -114,6 +114,8 @@ class RustMatrixTimeline( ) } + override val membershipChangeEventReceived: Flow = timelineDiffProcessor.membershipChangeEventReceived + init { Timber.d("Initialize timeline for room ${matrixRoom.roomId}") 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 c6eaa41462..c43542208d 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 @@ -95,6 +95,7 @@ class FakeMatrixRoom( private var joinRoomResult = Result.success(Unit) private var inviteUserResult = Result.success(Unit) private var canInviteResult = Result.success(true) + private var canKickResult = Result.success(false) private var canBanResult = Result.success(false) private var canRedactOwnResult = Result.success(canRedactOwn) private var canRedactOtherResult = Result.success(canRedactOther) @@ -111,6 +112,9 @@ class FakeMatrixRoom( private var cancelSendResult = Result.success(Unit) private var forwardEventResult = Result.success(Unit) private var reportContentResult = Result.success(Unit) + private var kickUserResult = Result.success(Unit) + private var banUserResult = Result.success(Unit) + private var unBanUserResult = Result.success(Unit) private var sendLocationResult = Result.success(Unit) private var createPollResult = Result.success(Unit) private var editPollResult = Result.success(Unit) @@ -299,6 +303,10 @@ class FakeMatrixRoom( return canBanResult } + override suspend fun canUserKick(userId: UserId): Result { + return canKickResult + } + override suspend fun canUserInvite(userId: UserId): Result { return canInviteResult } @@ -398,6 +406,18 @@ class FakeMatrixRoom( return reportContentResult } + override suspend fun kickUser(userId: UserId, reason: String?): Result { + return kickUserResult + } + + override suspend fun banUser(userId: UserId, reason: String?): Result { + return banUserResult + } + + override suspend fun unbanUser(userId: UserId, reason: String?): Result { + return unBanUserResult + } + val setIsFavoriteCalls = mutableListOf() override suspend fun setIsFavorite(isFavorite: Boolean): Result { @@ -522,6 +542,10 @@ class FakeMatrixRoom( joinRoomResult = result } + fun givenCanKickResult(result: Result) { + canKickResult = result + } + fun givenCanBanResult(result: Result) { canBanResult = result } @@ -598,6 +622,18 @@ class FakeMatrixRoom( reportContentResult = result } + fun givenKickUserResult(result: Result) { + kickUserResult = result + } + + fun givenBanUserResult(result: Result) { + banUserResult = result + } + + fun givenUnbanUserResult(result: Result) { + unBanUserResult = result + } + fun givenSendLocationResult(result: Result) { sendLocationResult = result } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeMatrixTimeline.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeMatrixTimeline.kt index 83ea98df6d..f9ab698b8e 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeMatrixTimeline.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeMatrixTimeline.kt @@ -24,6 +24,7 @@ import io.element.android.tests.testutils.simulateLongTask import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.getAndUpdate @@ -59,6 +60,8 @@ class FakeMatrixTimeline( override suspend fun paginateBackwards(requestSize: Int) = paginateBackwards() override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int) = paginateBackwards() + override val membershipChangeEventReceived = MutableSharedFlow() + private suspend fun paginateBackwards(): Result { updatePaginationState { copy(isBackPaginating = true) @@ -73,6 +76,10 @@ class FakeMatrixTimeline( return Result.success(Unit) } + fun givenMembershipChangeEventReceived() { + membershipChangeEventReceived.tryEmit(Unit) + } + override suspend fun fetchDetailsForEvent(eventId: EventId): Result = simulateLongTask { Result.success(Unit) } diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index b1442dcc51..ad1b804a80 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -114,6 +114,7 @@ "Audio" "Blocked users" "Bubbles" + "Call in progress (unsupported)" "Chat backup" "Copyright" "Creating room…" diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-4_5_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-4_5_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..1ab3b41f1a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-4_5_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4205303d8fea02149db6825e62e2df8b0ae647452e535c73521aefa43da5d3ae +size 18859 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-4_5_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-4_5_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a26644779e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-4_5_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b58661c91aa2e718518780ee7b217e5778bea9187060b9a35549c8f3f7b6ab4 +size 23198 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-4_5_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-4_5_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..0fa3b28936 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-4_5_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e223cda6d004bfd6c5310cd80f1ae3b3004e6e511bf6df55fde1093397562b60 +size 28428 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-4_5_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-4_5_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..61fabd05bb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-4_5_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7794f8f1fa3f7d9c7a1f37dae1d41635c6ce99dbb4b64c435e89b1c3bee4d0fd +size 10672 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-4_5_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-4_5_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f3a31ab492 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-4_5_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19afdcbcd76fa24242ddc6b61fed81169cd117db3a3cefe6a41df263be7e9a3e +size 10418 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-4_5_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-4_5_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ec2985b356 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-4_5_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:23bb2d5954ba402203dcb76b88f5da5f788d24d1f16e827671317d723802cfd7 +size 10496 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-4_5_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-4_5_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..394191aeb7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-4_5_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b11aa7970dd9f8e9a6ecb0cedea158e6e7070fd19c4dd3716d460bedebc14977 +size 8999 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-4_5_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-4_5_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f2df014dda --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-4_5_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:80fd575cdce74e271d92fb49258b79af0cfba2826320225830061a2be9b738d4 +size 30470 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-4_5_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-4_5_null_8,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9300adca4f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-4_5_null_8,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:68626eb427f6b0435b328110b6735b7a7c39f9e2204cd03742ace3e09609b836 +size 23282 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-4_5_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-4_5_null_9,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..665c8811ac --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-4_5_null_9,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b +size 4457 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Night-4_6_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Night-4_6_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..546123b226 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Night-4_6_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a397f9285a8ed44272d02343ac9418e4e84e359dcac67dad3725cc06449a1b42 +size 17574 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Night-4_6_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Night-4_6_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..21cd30c707 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Night-4_6_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:66e3caddece8e727af4b6da9382955916ebea52eb78ad814ad4a746827482b3d +size 21646 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Night-4_6_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Night-4_6_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..50ae5b2776 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Night-4_6_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ef8b93ca353aace2fce6cfb1608e58fdcac3ac60619c3b64409427127e27613 +size 26419 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Night-4_6_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Night-4_6_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..266a8ef8d4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Night-4_6_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a250c21e73b1b703c4283af4d3d1230fc58d56084792555a7233116919f2195 +size 9310 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Night-4_6_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Night-4_6_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..26d0df896a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Night-4_6_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0079374541069e230f68c164ec266ce09eddb3073a856aedddb68c80e2666dbd +size 8920 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Night-4_6_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Night-4_6_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..bd70b2f435 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Night-4_6_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:685bc2a0bd83489a92280da2bd17a44b2a4580493e985aa7809e338807c3fd40 +size 9139 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Night-4_6_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Night-4_6_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..5332d03050 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Night-4_6_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:550d0757162622ffb70750821d7af863647d02e9108686e6d19d9352516094d3 +size 7602 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Night-4_6_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Night-4_6_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b0f81d6edf --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Night-4_6_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e10ed25980d51632b8e6dc0c5b0f81c9b137d1882d28f3d74caee0fc852beb8a +size 25419 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Night-4_6_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Night-4_6_null_8,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..3a7110101d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Night-4_6_null_8,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4ca16fde118edd01fac50891e4d2154bf79b6d004a50c51270f1480aff5b948 +size 19252 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Night-4_6_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Night-4_6_null_9,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..fae8a6fca3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Night-4_6_null_9,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c89ac73df77c2bccb0c2aa80cee1420f78e7d07f0eda89a90bffef55e8cf753 +size 4464 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_RoomMemberList_null_RoomMemberList-Day-2_3_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_RoomMemberList_null_RoomMemberList-Day-2_3_null_8,NEXUS_5,1.0,en].png deleted file mode 100644 index 9960521ede..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_RoomMemberList_null_RoomMemberList-Day-2_3_null_8,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:efc266bca406673c57731b4d6a283d217e7199f4b8a9c70c45ed3e00611d165d -size 52344 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_RoomMemberList_null_RoomMemberList-Night-2_4_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_RoomMemberList_null_RoomMemberList-Night-2_4_null_8,NEXUS_5,1.0,en].png deleted file mode 100644 index 4107ebae71..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members_RoomMemberList_null_RoomMemberList-Night-2_4_null_8,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:65dd3de9356356a9bf651cecd725c1be89a0d1b1ba816e4df9b25ce0a76313dd -size 51139 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-5_6_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-6_7_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-5_6_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-6_7_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-5_6_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-6_7_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-5_6_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-6_7_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-5_6_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-6_7_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-5_6_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-6_7_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-5_6_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-6_7_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-5_6_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-6_7_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-5_6_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-6_7_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-5_6_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-6_7_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-5_6_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-6_7_null_5,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-5_6_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-6_7_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-5_6_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-6_7_null_6,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-5_6_null_6,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Day-6_7_null_6,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-5_7_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-6_8_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-5_7_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-6_8_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-5_7_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-6_8_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-5_7_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-6_8_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-5_7_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-6_8_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-5_7_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-6_8_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-5_7_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-6_8_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-5_7_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-6_8_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-5_7_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-6_8_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-5_7_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-6_8_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-5_7_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-6_8_null_5,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-5_7_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-6_8_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-5_7_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-6_8_null_6,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-5_7_null_6,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomNotificationSettings_null_RoomNotificationSettings-Night-6_8_null_6,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomPrivacyOption_null_RoomPrivacyOption-Day-4_5_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomPrivacyOption_null_RoomPrivacyOption-Day-5_6_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomPrivacyOption_null_RoomPrivacyOption-Day-4_5_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomPrivacyOption_null_RoomPrivacyOption-Day-5_6_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomPrivacyOption_null_RoomPrivacyOption-Night-4_6_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomPrivacyOption_null_RoomPrivacyOption-Night-5_7_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomPrivacyOption_null_RoomPrivacyOption-Night-4_6_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_RoomPrivacyOption_null_RoomPrivacyOption-Night-5_7_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettings_null_UserDefinedRoomNotificationSettings-Day-6_7_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettings_null_UserDefinedRoomNotificationSettings-Day-7_8_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettings_null_UserDefinedRoomNotificationSettings-Day-6_7_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettings_null_UserDefinedRoomNotificationSettings-Day-7_8_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettings_null_UserDefinedRoomNotificationSettings-Night-6_8_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettings_null_UserDefinedRoomNotificationSettings-Night-7_9_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettings_null_UserDefinedRoomNotificationSettings-Night-6_8_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettings_null_UserDefinedRoomNotificationSettings-Night-7_9_null_0,NEXUS_5,1.0,en].png 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-9_10_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from 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 rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_0,NEXUS_5,1.0,en].png 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-9_10_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from 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 rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_1,NEXUS_5,1.0,en].png 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-9_10_null_10,NEXUS_5,1.0,en].png similarity index 100% rename from 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 rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_10,NEXUS_5,1.0,en].png 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-9_10_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from 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 rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_2,NEXUS_5,1.0,en].png 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-9_10_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from 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 rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_3,NEXUS_5,1.0,en].png 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-9_10_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from 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 rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_4,NEXUS_5,1.0,en].png 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-9_10_null_5,NEXUS_5,1.0,en].png similarity index 100% rename from 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 rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_5,NEXUS_5,1.0,en].png 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-9_10_null_6,NEXUS_5,1.0,en].png similarity index 100% rename from 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 rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_6,NEXUS_5,1.0,en].png 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-9_10_null_7,NEXUS_5,1.0,en].png similarity index 100% rename from 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 rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_7,NEXUS_5,1.0,en].png 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-9_10_null_8,NEXUS_5,1.0,en].png similarity index 100% rename from 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 rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_8,NEXUS_5,1.0,en].png 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-9_10_null_9,NEXUS_5,1.0,en].png similarity index 100% rename from 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 rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_9,NEXUS_5,1.0,en].png 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-9_11_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from 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 rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_0,NEXUS_5,1.0,en].png 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-9_11_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from 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 rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_1,NEXUS_5,1.0,en].png 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-9_11_null_10,NEXUS_5,1.0,en].png similarity index 100% rename from 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 rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_10,NEXUS_5,1.0,en].png 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-9_11_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from 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 rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_2,NEXUS_5,1.0,en].png 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-9_11_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from 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 rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_3,NEXUS_5,1.0,en].png 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-9_11_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from 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 rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_4,NEXUS_5,1.0,en].png 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-9_11_null_5,NEXUS_5,1.0,en].png similarity index 100% rename from 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 rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_5,NEXUS_5,1.0,en].png 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-9_11_null_6,NEXUS_5,1.0,en].png similarity index 100% rename from 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 rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_6,NEXUS_5,1.0,en].png 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-9_11_null_7,NEXUS_5,1.0,en].png similarity index 100% rename from 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 rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_7,NEXUS_5,1.0,en].png 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-9_11_null_8,NEXUS_5,1.0,en].png similarity index 100% rename from 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 rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_8,NEXUS_5,1.0,en].png 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-9_11_null_9,NEXUS_5,1.0,en].png similarity index 100% rename from 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 rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_9,NEXUS_5,1.0,en].png 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-8_9_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from 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 rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Day-8_9_null_0,NEXUS_5,1.0,en].png 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-8_9_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from 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 rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Day-8_9_null_1,NEXUS_5,1.0,en].png 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-8_9_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from 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 rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Day-8_9_null_2,NEXUS_5,1.0,en].png 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-8_9_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from 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 rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Day-8_9_null_3,NEXUS_5,1.0,en].png 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-8_9_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from 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 rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Day-8_9_null_4,NEXUS_5,1.0,en].png 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-8_10_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from 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 rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Night-8_10_null_0,NEXUS_5,1.0,en].png 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-8_10_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from 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 rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Night-8_10_null_1,NEXUS_5,1.0,en].png 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-8_10_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from 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 rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Night-8_10_null_2,NEXUS_5,1.0,en].png 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-8_10_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from 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 rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Night-8_10_null_3,NEXUS_5,1.0,en].png 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-8_10_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from 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 rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions_RoomDetailsAdminSettingsView_null_RoomDetailsAdminSettingsView-Night-8_10_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_51,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_51,NEXUS_5,1.0,en].png index d69da80047..8fc639d820 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_51,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_51,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:820b23e9b9175b48461a4e7e359c2440b8c45cb598f512ea360519b0815621fc -size 18259 +oid sha256:81cae0afd5a66d246cd93035684629fccd05f4e142386e02e5b9135cfa6b85e1 +size 23046 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_52,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_52,NEXUS_5,1.0,en].png index 4cf9f8f839..2a8deba06a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_52,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_52,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ce4abbf622711cf6a106cb6c74ed729c6e609d329579b7b00ba8c1581b4b953 -size 17463 +oid sha256:7fbcbbd267665a5885e7a33f2b53dde1a8825e48500a3a7a99bc4fdb344c8f8a +size 21302 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_53,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_53,NEXUS_5,1.0,en].png index a475b2e7d1..0fafa7c13e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_53,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_53,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f0cb196c59e3bbd845f0e110150ac358ca78c9c08c212e184423ead5d841f67c -size 20219 +oid sha256:361d7af69a06439092ff379f8e929822d413bc3cb1831d6aa2d609a28084a866 +size 26675 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_54,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_54,NEXUS_5,1.0,en].png index a3d0076294..d69da80047 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_54,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_54,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2619a9a70eca12d5931ebc21adce48a1aed8383f35908bb09546f44b40f04543 -size 23094 +oid sha256:820b23e9b9175b48461a4e7e359c2440b8c45cb598f512ea360519b0815621fc +size 18259 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_55,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_55,NEXUS_5,1.0,en].png index 81b9668b0f..4cf9f8f839 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_55,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_55,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9d7d71d58b250bdec2d9b6e1ed46c5e3ffd98ffeefcaf4267b9979970a89750b -size 22226 +oid sha256:3ce4abbf622711cf6a106cb6c74ed729c6e609d329579b7b00ba8c1581b4b953 +size 17463 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_56,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_56,NEXUS_5,1.0,en].png index 5cb8c0a0bf..a475b2e7d1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_56,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_56,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e7bdea3caef3f1be9fac1fbb5511fe2d76b7985576c038f8c1d920615c3d49cd -size 25005 +oid sha256:f0cb196c59e3bbd845f0e110150ac358ca78c9c08c212e184423ead5d841f67c +size 20219 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_57,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_57,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a3d0076294 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_57,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2619a9a70eca12d5931ebc21adce48a1aed8383f35908bb09546f44b40f04543 +size 23094 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_58,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_58,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..81b9668b0f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_58,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d7d71d58b250bdec2d9b6e1ed46c5e3ffd98ffeefcaf4267b9979970a89750b +size 22226 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_59,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_59,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..5cb8c0a0bf --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_59,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e7bdea3caef3f1be9fac1fbb5511fe2d76b7985576c038f8c1d920615c3d49cd +size 25005