From 58d9b12ab304e817c74db62a6f300736bd76cdc6 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 6 May 2025 22:39:18 +0200 Subject: [PATCH 01/50] change (member moderation) : extract in a separate module --- features/roomdetails/impl/build.gradle.kts | 1 + .../roomdetails/impl/di/RoomDetailsModule.kt | 23 --- .../impl/members/RoomMemberListNode.kt | 14 ++ .../impl/members/RoomMemberListPresenter.kt | 16 +- .../impl/members/RoomMemberListState.kt | 4 +- .../members/RoomMemberListStateProvider.kt | 23 ++- .../impl/members/RoomMemberListView.kt | 11 +- .../moderation/ConfirmingRoomMemberAction.kt | 15 -- .../moderation/RoomMembersModerationEvents.kt | 21 --- .../moderation/RoomMembersModerationState.kt | 30 ---- .../RoomMembersModerationStateProvider.kt | 95 ------------ .../roommembermoderation/api/build.gradle.kts | 21 +++ .../api/RoomMemberModerationEvents.kt | 15 ++ .../api/RoomMemberModerationRenderer.kt | 20 +++ .../api/RoomMemberModerationState.kt | 23 +++ .../impl/build.gradle.kts | 39 +++++ .../DefaultRoomMemberModerationRenderer.kt | 37 +++++ .../InternalRoomMemberModerationEvents.kt | 17 +++ .../impl/InternalRoomMemberModerationState.kt | 32 ++++ .../impl/RoomMemberModerationPresenter.kt} | 144 ++++++++++-------- .../impl/RoomMemberModerationStateProvider.kt | 94 ++++++++++++ .../impl/RoomMemberModerationView.kt} | 110 +++++++------ .../impl/di/RoomMemberModerationModule.kt | 24 +++ .../src/main/res/values-be/translations.xml | 18 +++ .../src/main/res/values-cs/translations.xml | 21 +++ .../src/main/res/values-cy/translations.xml | 21 +++ .../src/main/res/values-de/translations.xml | 21 +++ .../src/main/res/values-el/translations.xml | 21 +++ .../src/main/res/values-es/translations.xml | 18 +++ .../src/main/res/values-et/translations.xml | 21 +++ .../src/main/res/values-eu/translations.xml | 16 ++ .../src/main/res/values-fa/translations.xml | 21 +++ .../src/main/res/values-fi/translations.xml | 21 +++ .../src/main/res/values-fr/translations.xml | 21 +++ .../src/main/res/values-hu/translations.xml | 21 +++ .../src/main/res/values-it/translations.xml | 18 +++ .../src/main/res/values-ka/translations.xml | 18 +++ .../src/main/res/values-nb/translations.xml | 21 +++ .../src/main/res/values-nl/translations.xml | 18 +++ .../src/main/res/values-pl/translations.xml | 21 +++ .../main/res/values-pt-rBR/translations.xml | 18 +++ .../src/main/res/values-pt/translations.xml | 19 +++ .../src/main/res/values-ro/translations.xml | 18 +++ .../src/main/res/values-ru/translations.xml | 20 +++ .../src/main/res/values-sk/translations.xml | 21 +++ .../src/main/res/values-sv/translations.xml | 21 +++ .../src/main/res/values-tr/translations.xml | 18 +++ .../src/main/res/values-uk/translations.xml | 21 +++ .../main/res/values-zh-rTW/translations.xml | 21 +++ .../src/main/res/values-zh/translations.xml | 21 +++ .../impl/src/main/res/values/localazy.xml | 21 +++ .../src/main/res/values-be/translations.xml | 9 -- .../src/main/res/values-cs/translations.xml | 12 -- .../src/main/res/values-cy/translations.xml | 12 -- .../src/main/res/values-de/translations.xml | 12 -- .../src/main/res/values-el/translations.xml | 12 -- .../src/main/res/values-es/translations.xml | 9 -- .../src/main/res/values-et/translations.xml | 12 -- .../src/main/res/values-eu/translations.xml | 8 - .../src/main/res/values-fa/translations.xml | 12 -- .../src/main/res/values-fi/translations.xml | 12 -- .../src/main/res/values-fr/translations.xml | 12 -- .../src/main/res/values-hu/translations.xml | 12 -- .../src/main/res/values-in/translations.xml | 9 -- .../src/main/res/values-it/translations.xml | 9 -- .../src/main/res/values-ka/translations.xml | 9 -- .../src/main/res/values-nb/translations.xml | 12 -- .../src/main/res/values-nl/translations.xml | 9 -- .../src/main/res/values-pl/translations.xml | 12 -- .../main/res/values-pt-rBR/translations.xml | 9 -- .../src/main/res/values-pt/translations.xml | 10 -- .../src/main/res/values-ro/translations.xml | 9 -- .../src/main/res/values-ru/translations.xml | 11 -- .../src/main/res/values-sk/translations.xml | 12 -- .../src/main/res/values-sv/translations.xml | 12 -- .../src/main/res/values-tr/translations.xml | 9 -- .../src/main/res/values-uk/translations.xml | 12 -- .../main/res/values-zh-rTW/translations.xml | 12 -- .../src/main/res/values-zh/translations.xml | 12 -- tools/localazy/config.json | 8 + 80 files changed, 1062 insertions(+), 633 deletions(-) delete mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomDetailsModule.kt delete mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/ConfirmingRoomMemberAction.kt delete mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationEvents.kt delete mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationState.kt delete mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationStateProvider.kt create mode 100644 features/roommembermoderation/api/build.gradle.kts create mode 100644 features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationEvents.kt create mode 100644 features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationRenderer.kt create mode 100644 features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationState.kt create mode 100644 features/roommembermoderation/impl/build.gradle.kts create mode 100644 features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/DefaultRoomMemberModerationRenderer.kt create mode 100644 features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationEvents.kt create mode 100644 features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationState.kt rename features/{roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenter.kt => roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt} (53%) create mode 100644 features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationStateProvider.kt rename features/{roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationView.kt => roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt} (72%) create mode 100644 features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/di/RoomMemberModerationModule.kt create mode 100644 features/roommembermoderation/impl/src/main/res/values-be/translations.xml create mode 100644 features/roommembermoderation/impl/src/main/res/values-cs/translations.xml create mode 100644 features/roommembermoderation/impl/src/main/res/values-cy/translations.xml create mode 100644 features/roommembermoderation/impl/src/main/res/values-de/translations.xml create mode 100644 features/roommembermoderation/impl/src/main/res/values-el/translations.xml create mode 100644 features/roommembermoderation/impl/src/main/res/values-es/translations.xml create mode 100644 features/roommembermoderation/impl/src/main/res/values-et/translations.xml create mode 100644 features/roommembermoderation/impl/src/main/res/values-eu/translations.xml create mode 100644 features/roommembermoderation/impl/src/main/res/values-fa/translations.xml create mode 100644 features/roommembermoderation/impl/src/main/res/values-fi/translations.xml create mode 100644 features/roommembermoderation/impl/src/main/res/values-fr/translations.xml create mode 100644 features/roommembermoderation/impl/src/main/res/values-hu/translations.xml create mode 100644 features/roommembermoderation/impl/src/main/res/values-it/translations.xml create mode 100644 features/roommembermoderation/impl/src/main/res/values-ka/translations.xml create mode 100644 features/roommembermoderation/impl/src/main/res/values-nb/translations.xml create mode 100644 features/roommembermoderation/impl/src/main/res/values-nl/translations.xml create mode 100644 features/roommembermoderation/impl/src/main/res/values-pl/translations.xml create mode 100644 features/roommembermoderation/impl/src/main/res/values-pt-rBR/translations.xml create mode 100644 features/roommembermoderation/impl/src/main/res/values-pt/translations.xml create mode 100644 features/roommembermoderation/impl/src/main/res/values-ro/translations.xml create mode 100644 features/roommembermoderation/impl/src/main/res/values-ru/translations.xml create mode 100644 features/roommembermoderation/impl/src/main/res/values-sk/translations.xml create mode 100644 features/roommembermoderation/impl/src/main/res/values-sv/translations.xml create mode 100644 features/roommembermoderation/impl/src/main/res/values-tr/translations.xml create mode 100644 features/roommembermoderation/impl/src/main/res/values-uk/translations.xml create mode 100644 features/roommembermoderation/impl/src/main/res/values-zh-rTW/translations.xml create mode 100644 features/roommembermoderation/impl/src/main/res/values-zh/translations.xml create mode 100644 features/roommembermoderation/impl/src/main/res/values/localazy.xml diff --git a/features/roomdetails/impl/build.gradle.kts b/features/roomdetails/impl/build.gradle.kts index 87ca1afe4d..a23997b43d 100644 --- a/features/roomdetails/impl/build.gradle.kts +++ b/features/roomdetails/impl/build.gradle.kts @@ -54,6 +54,7 @@ dependencies { implementation(projects.features.knockrequests.api) implementation(projects.features.verifysession.api) implementation(projects.features.reportroom.api) + implementation(projects.features.roommembermoderation.api) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomDetailsModule.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomDetailsModule.kt deleted file mode 100644 index 0361cfb2ca..0000000000 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomDetailsModule.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.roomdetails.impl.di - -import com.squareup.anvil.annotations.ContributesTo -import dagger.Binds -import dagger.Module -import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationPresenter -import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationState -import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.di.RoomScope - -@Module -@ContributesTo(RoomScope::class) -interface RoomDetailsModule { - @Binds - fun bindRoomMembersModerationPresenter(presenter: RoomMembersModerationPresenter): Presenter -} 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 b1eb471391..05a35fad88 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 @@ -18,6 +18,9 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import im.vector.app.features.analytics.plan.MobileScreen import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.roommembermoderation.api.ModerationAction +import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationRenderer import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.UserId import io.element.android.services.analytics.api.AnalyticsService @@ -28,6 +31,7 @@ class RoomMemberListNode @AssistedInject constructor( @Assisted plugins: List, presenterFactory: RoomMemberListPresenter.Factory, private val analyticsService: AnalyticsService, + private val roomMemberModerationRenderer: RoomMemberModerationRenderer, ) : Node(buildContext, plugins = plugins), RoomMemberListNavigator { interface Callback : Plugin { fun openRoomMemberDetails(roomMemberId: UserId) @@ -69,6 +73,16 @@ class RoomMemberListNode @AssistedInject constructor( modifier = modifier, navigator = this, ) + roomMemberModerationRenderer.Render( + state = state.moderationState, + onSelectAction = { action -> + when (action) { + is ModerationAction.DisplayProfile -> openRoomMemberDetails(action.member.userId) + else -> state.moderationState.eventSink(RoomMemberModerationEvents.ProcessAction(action)) + } + }, + modifier = Modifier, + ) } } 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 e2527444aa..07688387e3 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 @@ -19,8 +19,9 @@ 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.RoomMembersModerationState +import io.element.android.features.roommembermoderation.api.ModerationAction +import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers @@ -34,6 +35,7 @@ import io.element.android.libraries.matrix.api.room.RoomMembersState import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.ui.room.canInviteAsState +import io.element.android.libraries.matrix.ui.room.isDmAsState import io.element.android.libraries.matrix.ui.room.roomMemberIdentityStateChange import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.persistentMapOf @@ -48,7 +50,7 @@ class RoomMemberListPresenter @AssistedInject constructor( private val room: JoinedRoom, private val roomMemberListDataSource: RoomMemberListDataSource, private val coroutineDispatchers: CoroutineDispatchers, - private val roomMembersModerationPresenter: Presenter, + private val roomMembersModerationPresenter: Presenter, private val encryptionService: EncryptionService, @Assisted private val navigator: RoomMemberListNavigator, ) : Presenter { @@ -69,7 +71,7 @@ class RoomMemberListPresenter @AssistedInject constructor( val membersState by room.membersStateFlow.collectAsState() val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val canInvite by room.canInviteAsState(syncUpdateFlow.value) - + val isDm = room.isDmAsState() val roomModerationState = roomMembersModerationPresenter.present() val roomMemberIdentityStates by produceState(persistentMapOf()) { @@ -163,8 +165,10 @@ class RoomMemberListPresenter @AssistedInject constructor( is RoomMemberListEvents.OnSearchActiveChanged -> isSearchActive = event.active is RoomMemberListEvents.UpdateSearchQuery -> searchQuery = event.query is RoomMemberListEvents.RoomMemberSelected -> - if (roomModerationState.canDisplayModerationActions) { - roomModerationState.eventSink(RoomMembersModerationEvents.SelectRoomMember(event.roomMember)) + if (event.roomMember.membership == RoomMembershipState.BAN) { + roomModerationState.eventSink(RoomMemberModerationEvents.ProcessAction(ModerationAction.UnbanUser(event.roomMember))) + } else if (!isDm.value && (roomModerationState.canBan || roomModerationState.canKick)) { + roomModerationState.eventSink(RoomMemberModerationEvents.RenderActions(event.roomMember)) } else { navigator.openRoomMemberDetails(event.roomMember.userId) } 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 b5fa6c37a9..8e492e5371 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 @@ -7,7 +7,7 @@ package io.element.android.features.roomdetails.impl.members -import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationState +import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.api.encryption.identity.IdentityState @@ -20,7 +20,7 @@ data class RoomMemberListState( val searchResults: SearchBarResultState>, val isSearchActive: Boolean, val canInvite: Boolean, - val moderationState: RoomMembersModerationState, + val moderationState: RoomMemberModerationState, 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 6fec01c675..09a7d9c910 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 @@ -8,8 +8,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.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationState 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 @@ -87,7 +87,7 @@ internal class RoomMemberListStateBannedProvider : PreviewParameterProvider = AsyncData.Loading(), searchResults: SearchBarResultState> = SearchBarResultState.Initial(), - moderationState: RoomMembersModerationState = aRoomMembersModerationState(), + moderationState: RoomMemberModerationState = aRoomMemberModerationState(), ) = RoomMemberListState( roomMembers = roomMembers, searchQuery = "", @@ -130,6 +130,17 @@ internal fun aRoomMemberListState( eventSink = {} ) +fun aRoomMemberModerationState( + canBan: Boolean = false, + canKick: Boolean = false, +): RoomMemberModerationState { + return object : RoomMemberModerationState { + override val canKick: Boolean = canKick + override val canBan: Boolean = canBan + override val eventSink: (RoomMemberModerationEvents) -> Unit = {} + } +} + fun aRoomMember( userId: UserId = UserId("@alice:server.org"), displayName: String? = null, 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 875606f948..fab12adb4a 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 @@ -45,7 +45,7 @@ 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.features.roomdetails.impl.members.moderation.RoomMembersModerationView +import io.element.android.features.roommembermoderation.api.RoomMemberModerationState 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 @@ -99,7 +99,7 @@ fun RoomMemberListView( } ) { padding -> var selectedSection by remember { mutableStateOf(SelectedSection.entries[initialSelectedSectionIndex]) } - if (!state.moderationState.canDisplayBannedUsers && selectedSection == SelectedSection.BANNED) { + if (!state.moderationState.canBan && selectedSection == SelectedSection.BANNED) { SideEffect { selectedSection = SelectedSection.MEMBERS } @@ -127,7 +127,7 @@ fun RoomMemberListView( RoomMemberList( roomMembers = state.roomMembers, showMembersCount = true, - canDisplayBannedUsersControls = state.moderationState.canDisplayBannedUsers, + canDisplayBannedUsersControls = state.moderationState.canBan, selectedSection = selectedSection, onSelectedSectionChange = { selectedSection = it }, onSelectUser = ::onSelectUser, @@ -135,11 +135,6 @@ fun RoomMemberListView( } } } - - RoomMembersModerationView( - state = state.moderationState, - onDisplayMemberProfile = navigator::openRoomMemberDetails - ) } @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/ConfirmingRoomMemberAction.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/ConfirmingRoomMemberAction.kt deleted file mode 100644 index b561dc9ac7..0000000000 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/ConfirmingRoomMemberAction.kt +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.roomdetails.impl.members.moderation - -import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.matrix.api.room.RoomMember - -data class ConfirmingRoomMemberAction( - val roomMember: RoomMember, -) : AsyncAction.Confirming 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 deleted file mode 100644 index eaabc8e4c2..0000000000 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationEvents.kt +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.roomdetails.impl.members.moderation - -import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.room.RoomMember - -sealed interface RoomMembersModerationEvents { - data class SelectRoomMember(val roomMember: RoomMember) : RoomMembersModerationEvents - data object KickUser : RoomMembersModerationEvents - data class DoKickUser(val reason: String) : RoomMembersModerationEvents - data object BanUser : RoomMembersModerationEvents - data class DoBanUser(val reason: String) : RoomMembersModerationEvents - data class UnbanUser(val userId: UserId) : RoomMembersModerationEvents - data object Reset : RoomMembersModerationEvents -} 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 deleted file mode 100644 index a6c9a3e704..0000000000 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationState.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.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 canDisplayModerationActions: Boolean, - 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/RoomMembersModerationStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationStateProvider.kt deleted file mode 100644 index 9139100980..0000000000 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationStateProvider.kt +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.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.toPersistentList - -class RoomMembersModerationStateProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - aRoomMembersModerationState( - selectedRoomMember = anAlice(), - actions = listOf( - ModerationAction.DisplayProfile(anAlice().userId), - ), - ), - aRoomMembersModerationState( - selectedRoomMember = anAlice(), - actions = listOf( - ModerationAction.DisplayProfile(anAlice().userId), - ModerationAction.KickUser(userId = anAlice().userId), - ), - ), - aRoomMembersModerationState( - selectedRoomMember = anAlice(), - actions = listOf( - ModerationAction.DisplayProfile(anAlice().userId), - ModerationAction.KickUser(userId = anAlice().userId), - ModerationAction.BanUser(userId = anAlice().userId), - ), - ), - aRoomMembersModerationState( - selectedRoomMember = anAlice(), - kickUserAsyncAction = AsyncAction.ConfirmingNoParams, - ), - aRoomMembersModerationState( - selectedRoomMember = anAlice(), - kickUserAsyncAction = AsyncAction.Loading, - ), - aRoomMembersModerationState( - selectedRoomMember = anAlice(), - banUserAsyncAction = AsyncAction.ConfirmingNoParams, - ), - 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(), - unbanUserAsyncAction = ConfirmingRoomMemberAction(anAlice()), - ), - aRoomMembersModerationState( - kickUserAsyncAction = AsyncAction.Success(Unit), - banUserAsyncAction = AsyncAction.Success(Unit), - unbanUserAsyncAction = AsyncAction.Success(Unit), - ), - ) -} - -fun aRoomMembersModerationState( - canDisplayModerationActions: Boolean = false, - 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( - canDisplayModerationActions = canDisplayModerationActions, - selectedRoomMember = selectedRoomMember, - actions = actions.toPersistentList(), - kickUserAsyncAction = kickUserAsyncAction, - banUserAsyncAction = banUserAsyncAction, - unbanUserAsyncAction = unbanUserAsyncAction, - canDisplayBannedUsers = canDisplayBannedUsers, - eventSink = eventSink, -) diff --git a/features/roommembermoderation/api/build.gradle.kts b/features/roommembermoderation/api/build.gradle.kts new file mode 100644 index 0000000000..aeea031757 --- /dev/null +++ b/features/roommembermoderation/api/build.gradle.kts @@ -0,0 +1,21 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +plugins { + id("io.element.android-compose-library") +} + +android { + namespace = "io.element.android.features.roommembermoderation.api" +} + +dependencies { + implementation(projects.libraries.architecture) + implementation(projects.libraries.designsystem) + implementation(projects.libraries.uiStrings) + implementation(projects.libraries.matrix.api) +} diff --git a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationEvents.kt b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationEvents.kt new file mode 100644 index 0000000000..265a59a125 --- /dev/null +++ b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationEvents.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.roommembermoderation.api + +import io.element.android.libraries.matrix.api.room.RoomMember + +interface RoomMemberModerationEvents { + data class RenderActions(val roomMember: RoomMember) : RoomMemberModerationEvents + data class ProcessAction(val action: ModerationAction): RoomMemberModerationEvents +} diff --git a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationRenderer.kt b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationRenderer.kt new file mode 100644 index 0000000000..2911bf1b8b --- /dev/null +++ b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationRenderer.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.roommembermoderation.api + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +interface RoomMemberModerationRenderer { + @Composable + fun Render( + state: RoomMemberModerationState, + onSelectAction: (ModerationAction) -> Unit, + modifier: Modifier, + ) +} diff --git a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationState.kt b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationState.kt new file mode 100644 index 0000000000..410b8d6423 --- /dev/null +++ b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationState.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.roommembermoderation.api + +import io.element.android.libraries.matrix.api.room.RoomMember + +interface RoomMemberModerationState { + val canKick: Boolean + val canBan: Boolean + val eventSink: (RoomMemberModerationEvents) -> Unit +} + +sealed interface ModerationAction { + data class DisplayProfile(val member: RoomMember) : ModerationAction + data class KickUser(val member: RoomMember) : ModerationAction + data class BanUser(val member: RoomMember) : ModerationAction + data class UnbanUser(val member: RoomMember) : ModerationAction +} diff --git a/features/roommembermoderation/impl/build.gradle.kts b/features/roommembermoderation/impl/build.gradle.kts new file mode 100644 index 0000000000..3d566077eb --- /dev/null +++ b/features/roommembermoderation/impl/build.gradle.kts @@ -0,0 +1,39 @@ +import extension.setupAnvil + +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +plugins { + id("io.element.android-compose-library") +} + +android { + namespace = "io.element.android.features.roommembermoderation.impl" +} + +setupAnvil() + +dependencies { + implementation(projects.libraries.core) + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrixui) + api(projects.features.roommembermoderation.api) + implementation(projects.libraries.designsystem) + implementation(projects.libraries.uiStrings) + implementation(projects.services.analytics.compose) + + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.coroutines.core) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) + testImplementation(projects.libraries.matrix.test) + testImplementation(projects.tests.testutils) + testImplementation(projects.services.analytics.test) +} diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/DefaultRoomMemberModerationRenderer.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/DefaultRoomMemberModerationRenderer.kt new file mode 100644 index 0000000000..9a9cce3eb7 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/DefaultRoomMemberModerationRenderer.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.roommembermoderation.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.Modifier +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.roommembermoderation.api.ModerationAction +import io.element.android.features.roommembermoderation.api.RoomMemberModerationRenderer +import io.element.android.features.roommembermoderation.api.RoomMemberModerationState +import io.element.android.libraries.di.RoomScope +import timber.log.Timber +import javax.inject.Inject + +@ContributesBinding(RoomScope::class) +class DefaultRoomMemberModerationRenderer @Inject constructor() : RoomMemberModerationRenderer { + @Composable + override fun Render( + state: RoomMemberModerationState, + onSelectAction: (ModerationAction) -> Unit, + modifier: Modifier + ) { + if (state is InternalRoomMemberModerationState) { + RoomMemberModerationView(state, onSelectAction, modifier) + } else { + SideEffect { + Timber.d("RoomMemberModerationRenderer: Render called with unsupported state: $state") + } + } + } +} diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationEvents.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationEvents.kt new file mode 100644 index 0000000000..206a379698 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationEvents.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.roommembermoderation.impl + +import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents + +sealed interface InternalRoomMemberModerationEvents: RoomMemberModerationEvents { + data class DoKickUser(val reason: String) : InternalRoomMemberModerationEvents + data class DoBanUser(val reason: String) : InternalRoomMemberModerationEvents + data object DoUnbanUser : InternalRoomMemberModerationEvents + data object Reset : InternalRoomMemberModerationEvents +} diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationState.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationState.kt new file mode 100644 index 0000000000..e6258f2003 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationState.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.roommembermoderation.impl + +import io.element.android.features.roommembermoderation.api.ModerationAction +import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationState +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.room.RoomMember +import kotlinx.collections.immutable.ImmutableList + +data class InternalRoomMemberModerationState( + override val canKick: Boolean, + override val canBan: Boolean, + val selectedRoomMember: AsyncData, + val actions: ImmutableList, + val kickUserAsyncAction: AsyncAction, + val banUserAsyncAction: AsyncAction, + val unbanUserAsyncAction: AsyncAction, + override val eventSink: (RoomMemberModerationEvents) -> Unit, +) : RoomMemberModerationState { + + val canOnlyDisplayProfile = actions.size == 1 && actions.first() is ModerationAction.DisplayProfile + val canDisplayActions = actions.isNotEmpty() && !canOnlyDisplayProfile +} + diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenter.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt similarity index 53% rename from features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenter.kt rename to features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt index af71e21dff..5f7fa80c60 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenter.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt @@ -5,31 +5,34 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomdetails.impl.members.moderation +package io.element.android.features.roommembermoderation.impl import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import im.vector.app.features.analytics.plan.RoomModeration +import io.element.android.features.roommembermoderation.api.ModerationAction +import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runUpdatingState import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.RoomMember -import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.ui.room.canBanAsState import io.element.android.libraries.matrix.ui.room.canKickAsState -import io.element.android.libraries.matrix.ui.room.isDmAsState import io.element.android.libraries.matrix.ui.room.userPowerLevelAsState import io.element.android.services.analytics.api.AnalyticsService +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.drop @@ -37,45 +40,20 @@ import kotlinx.coroutines.flow.take import kotlinx.coroutines.launch import javax.inject.Inject -class RoomMembersModerationPresenter @Inject constructor( +class RoomMemberModerationPresenter @Inject constructor( private val room: JoinedRoom, private val dispatchers: CoroutineDispatchers, private val analyticsService: AnalyticsService, -) : Presenter { - private var selectedMember by mutableStateOf(null) +) : Presenter { + private var selectedMember by mutableStateOf>(AsyncData.Uninitialized) @Composable - override fun present(): RoomMembersModerationState { + override fun present(): RoomMemberModerationState { val coroutineScope = rememberCoroutineScope() val syncUpdateFlow = room.syncUpdateFlow.collectAsState() - val canBan by room.canBanAsState(syncUpdateFlow.value) - val canKick by room.canKickAsState(syncUpdateFlow.value) - val isDm by room.isDmAsState() - val currentUserMemberPowerLevel by room.userPowerLevelAsState(syncUpdateFlow.value) - - val canDisplayModerationActions by remember { - derivedStateOf { !isDm && (canBan || canKick) } - } - val canDisplayBannedUsers by remember { - derivedStateOf { !isDm && canBan } - } - val moderationActions by remember { - derivedStateOf { - buildList { - selectedMember?.let { roomMember -> - add(ModerationAction.DisplayProfile(roomMember.userId)) - if (currentUserMemberPowerLevel > roomMember.powerLevel) { - if (canKick) { - add(ModerationAction.KickUser(roomMember.userId)) - } - if (canBan) { - add(ModerationAction.BanUser(roomMember.userId)) - } - } - } - }.toPersistentList() - } - } + val canBan = room.canBanAsState(syncUpdateFlow.value) + val canKick = room.canKickAsState(syncUpdateFlow.value) + val currentUserMemberPowerLevel = room.userPowerLevelAsState(syncUpdateFlow.value) val kickUserAsyncAction = remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction) } @@ -84,60 +62,92 @@ class RoomMembersModerationPresenter @Inject constructor( val unbanUserAsyncAction = remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction) } - fun handleEvent(event: RoomMembersModerationEvents) { + val moderationActions = remember { mutableStateOf(persistentListOf()) } + + fun handleEvent(event: RoomMemberModerationEvents) { when (event) { - is RoomMembersModerationEvents.SelectRoomMember -> { - if (event.roomMember.membership == RoomMembershipState.BAN && canBan) { - // In this case the view will render a dialog to confirm the unbanning of the user - unbanUserAsyncAction.value = ConfirmingRoomMemberAction(event.roomMember) - } else { - // In this case the view will render a bottom sheet. - selectedMember = event.roomMember + is RoomMemberModerationEvents.RenderActions -> { + selectedMember = AsyncData.Success(event.roomMember) + moderationActions.value = computeModerationActions( + member = event.roomMember, + canKick = canKick.value, + canBan = canBan.value, + currentUserMemberPowerLevel = currentUserMemberPowerLevel.value, + ) + } + is RoomMemberModerationEvents.ProcessAction -> { + when(val action = event.action) { + is ModerationAction.DisplayProfile -> Unit + is ModerationAction.KickUser -> { + selectedMember = AsyncData.Success(action.member) + kickUserAsyncAction.value = AsyncAction.ConfirmingNoParams + } + is ModerationAction.BanUser -> { + selectedMember = AsyncData.Success(action.member) + banUserAsyncAction.value = AsyncAction.ConfirmingNoParams + } + is ModerationAction.UnbanUser -> { + selectedMember = AsyncData.Success(action.member) + unbanUserAsyncAction.value = AsyncAction.ConfirmingNoParams + } } } - is RoomMembersModerationEvents.KickUser -> { - kickUserAsyncAction.value = AsyncAction.ConfirmingNoParams - } - is RoomMembersModerationEvents.DoKickUser -> { - selectedMember?.let { + is InternalRoomMemberModerationEvents.DoKickUser -> { + selectedMember.dataOrNull()?.let { coroutineScope.kickUser(it.userId, event.reason, kickUserAsyncAction) } - selectedMember = null + selectedMember = AsyncData.Uninitialized } - is RoomMembersModerationEvents.BanUser -> { - banUserAsyncAction.value = AsyncAction.ConfirmingNoParams - } - is RoomMembersModerationEvents.DoBanUser -> { - selectedMember?.let { + is InternalRoomMemberModerationEvents.DoBanUser -> { + selectedMember.dataOrNull()?.let { coroutineScope.banUser(it.userId, event.reason, banUserAsyncAction) } - selectedMember = null + selectedMember = AsyncData.Uninitialized } - is RoomMembersModerationEvents.UnbanUser -> { - // We are already confirming when we are reaching this point - coroutineScope.unbanUser(event.userId, unbanUserAsyncAction) - } - is RoomMembersModerationEvents.Reset -> { - selectedMember = null + is InternalRoomMemberModerationEvents.Reset -> { + selectedMember = AsyncData.Uninitialized kickUserAsyncAction.value = AsyncAction.Uninitialized banUserAsyncAction.value = AsyncAction.Uninitialized unbanUserAsyncAction.value = AsyncAction.Uninitialized } + is InternalRoomMemberModerationEvents.DoUnbanUser -> { + selectedMember.dataOrNull()?.let { + coroutineScope.unbanUser(it.userId, unbanUserAsyncAction) + } + selectedMember = AsyncData.Uninitialized + } } } - return RoomMembersModerationState( - canDisplayModerationActions = canDisplayModerationActions, + return InternalRoomMemberModerationState( + canKick = canKick.value, + canBan = canBan.value, selectedRoomMember = selectedMember, - actions = moderationActions, + actions = moderationActions.value, kickUserAsyncAction = kickUserAsyncAction.value, banUserAsyncAction = banUserAsyncAction.value, unbanUserAsyncAction = unbanUserAsyncAction.value, - canDisplayBannedUsers = canDisplayBannedUsers, eventSink = { handleEvent(it) }, ) } + private fun computeModerationActions( + member: RoomMember, + canKick: Boolean, + canBan: Boolean, + currentUserMemberPowerLevel: Long, + ): PersistentList { + return buildList { + add(ModerationAction.DisplayProfile(member)) + if (canKick && member.powerLevel < currentUserMemberPowerLevel) { + add(ModerationAction.KickUser(member)) + } + if (canBan && member.powerLevel < currentUserMemberPowerLevel) { + add(ModerationAction.BanUser(member)) + } + }.toPersistentList() + } + private fun CoroutineScope.kickUser( userId: UserId, reason: String, @@ -167,7 +177,7 @@ class RoomMembersModerationPresenter @Inject constructor( unbanUserAction: MutableState>, ) = runActionAndWaitForMembershipChange(unbanUserAction) { analyticsService.capture(RoomModeration(RoomModeration.Action.UnbanMember)) - room.unbanUser(userId) + room.unbanUser(userId = userId) } private fun CoroutineScope.runActionAndWaitForMembershipChange( diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationStateProvider.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationStateProvider.kt new file mode 100644 index 0000000000..b705e5217d --- /dev/null +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationStateProvider.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.roommembermoderation.impl + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.roommembermoderation.api.ModerationAction +import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.AsyncData +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.RoomMembershipState +import kotlinx.collections.immutable.toPersistentList + +class RoomMemberModerationStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aRoomMembersModerationState( + selectedRoomMember = AsyncData.Success(anAlice()), + actions = listOf( + ModerationAction.DisplayProfile(anAlice()), + ), + ), + aRoomMembersModerationState( + selectedRoomMember = AsyncData.Success(anAlice()), + actions = listOf( + ModerationAction.DisplayProfile(anAlice()), + ModerationAction.KickUser(anAlice()), + ), + ), + aRoomMembersModerationState( + selectedRoomMember = AsyncData.Success(anAlice()), + actions = listOf( + ModerationAction.DisplayProfile(anAlice()), + ModerationAction.KickUser(anAlice()), + ModerationAction.BanUser(anAlice()), + ), + ), + aRoomMembersModerationState( + selectedRoomMember = AsyncData.Success(anAlice()), + kickUserAsyncAction = AsyncAction.ConfirmingNoParams, + ), + aRoomMembersModerationState( + selectedRoomMember = AsyncData.Success(anAlice()), + kickUserAsyncAction = AsyncAction.Loading, + ), + aRoomMembersModerationState( + selectedRoomMember = AsyncData.Success(anAlice()), + banUserAsyncAction = AsyncAction.ConfirmingNoParams, + ), + aRoomMembersModerationState( + selectedRoomMember = AsyncData.Success(anAlice()), + banUserAsyncAction = AsyncAction.Loading, + ), + ) +} + +fun anAlice() = RoomMember( + UserId(value = "@alice:server.org"), + displayName = "Alice", + avatarUrl = null, + role = RoomMember.Role.forPowerLevel(100L), + membership = RoomMembershipState.JOIN, + isNameAmbiguous = false, + powerLevel = 100L, + normalizedPowerLevel = 100L, + isIgnored = false, + membershipChangeReason = null, +) + +fun aRoomMembersModerationState( + canKick: Boolean = false, + canBan: Boolean = false, + selectedRoomMember: AsyncData = AsyncData.Uninitialized, + actions: List = emptyList(), + kickUserAsyncAction: AsyncAction = AsyncAction.Uninitialized, + banUserAsyncAction: AsyncAction = AsyncAction.Uninitialized, + unbanUserAsyncAction: AsyncAction = AsyncAction.Uninitialized, + eventSink: (RoomMemberModerationEvents) -> Unit = {}, +) = InternalRoomMemberModerationState( + canKick = canKick, + canBan = canBan, + selectedRoomMember = selectedRoomMember, + actions = actions.toPersistentList(), + kickUserAsyncAction = kickUserAsyncAction, + banUserAsyncAction = banUserAsyncAction, + unbanUserAsyncAction = unbanUserAsyncAction, + eventSink = eventSink, +) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationView.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt similarity index 72% rename from features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationView.kt rename to features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt index 535a3fc5b7..ff949fde59 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationView.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.roomdetails.impl.members.moderation +package io.element.android.features.roommembermoderation.impl import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -20,7 +20,9 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -30,7 +32,7 @@ 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.features.roommembermoderation.api.ModerationAction 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 @@ -47,7 +49,6 @@ 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.matrix.ui.model.getAvatarData @@ -57,32 +58,27 @@ import kotlinx.coroutines.launch import timber.log.Timber @Composable -fun RoomMembersModerationView( - state: RoomMembersModerationState, - onDisplayMemberProfile: (UserId) -> Unit, +fun RoomMemberModerationView( + state: InternalRoomMemberModerationState, + onSelectAction: (ModerationAction) -> Unit, modifier: Modifier = Modifier, ) { + val selectedRoomMember = state.selectedRoomMember.dataOrNull() Box(modifier = modifier) { - if (state.selectedRoomMember != null && state.actions.isNotEmpty()) { + if (selectedRoomMember != null && state.canDisplayActions) { RoomMemberActionsBottomSheet( - roomMember = state.selectedRoomMember, + roomMember = selectedRoomMember, actions = state.actions, - onSelectAction = { 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) }, + onSelectAction = onSelectAction, + onDismiss = { state.eventSink(InternalRoomMemberModerationEvents.Reset) }, ) } + val onSelectAction by rememberUpdatedState(onSelectAction) + LaunchedEffect(state.canOnlyDisplayProfile) { + if (state.canOnlyDisplayProfile) { + onSelectAction(state.actions.first()) + } + } val asyncIndicatorState = rememberAsyncIndicatorState() AsyncIndicatorHost(modifier = Modifier.statusBarsPadding(), state = asyncIndicatorState) @@ -90,23 +86,23 @@ fun RoomMembersModerationView( when (val action = state.kickUserAsyncAction) { is AsyncAction.Confirming -> { TextFieldDialog( - title = stringResource(CommonStrings.screen_bottom_sheet_manage_room_member_kick_member_confirmation_title), - submitText = stringResource(CommonStrings.screen_bottom_sheet_manage_room_member_kick_member_confirmation_action), + title = stringResource(R.string.screen_bottom_sheet_manage_room_member_kick_member_confirmation_title), + submitText = stringResource(R.string.screen_bottom_sheet_manage_room_member_kick_member_confirmation_action), onSubmit = { reason -> - state.eventSink(RoomMembersModerationEvents.DoKickUser(reason = reason)) + state.eventSink(InternalRoomMemberModerationEvents.DoKickUser(reason = reason)) }, - onDismissRequest = { state.eventSink(RoomMembersModerationEvents.Reset) }, + onDismissRequest = { state.eventSink(InternalRoomMemberModerationEvents.Reset) }, placeholder = stringResource(id = CommonStrings.common_reason), label = stringResource(id = CommonStrings.common_reason), - content = stringResource(CommonStrings.screen_bottom_sheet_manage_room_member_kick_member_confirmation_description), + content = stringResource(R.string.screen_bottom_sheet_manage_room_member_kick_member_confirmation_description), value = "", ) } is AsyncAction.Loading -> { LaunchedEffect(action) { - val userDisplayName = state.selectedRoomMember?.getBestName().orEmpty() + val userDisplayName = selectedRoomMember?.getBestName().orEmpty() asyncIndicatorState.enqueue { - AsyncIndicator.Loading(text = stringResource(CommonStrings.screen_bottom_sheet_manage_room_member_removing_user, userDisplayName)) + AsyncIndicator.Loading(text = stringResource(R.string.screen_bottom_sheet_manage_room_member_removing_user, userDisplayName)) } } } @@ -129,23 +125,23 @@ fun RoomMembersModerationView( when (val action = state.banUserAsyncAction) { is AsyncAction.Confirming -> { TextFieldDialog( - title = stringResource(CommonStrings.screen_bottom_sheet_manage_room_member_ban_member_confirmation_title), - submitText = stringResource(CommonStrings.screen_bottom_sheet_manage_room_member_ban_member_confirmation_action), + title = stringResource(R.string.screen_bottom_sheet_manage_room_member_ban_member_confirmation_title), + submitText = stringResource(R.string.screen_bottom_sheet_manage_room_member_ban_member_confirmation_action), onSubmit = { reason -> - state.eventSink(RoomMembersModerationEvents.DoBanUser(reason = reason)) + state.eventSink(InternalRoomMemberModerationEvents.DoBanUser(reason = reason)) }, - onDismissRequest = { state.eventSink(RoomMembersModerationEvents.Reset) }, + onDismissRequest = { state.eventSink(InternalRoomMemberModerationEvents.Reset) }, placeholder = stringResource(id = CommonStrings.common_reason), label = stringResource(id = CommonStrings.common_reason), - content = stringResource(CommonStrings.screen_bottom_sheet_manage_room_member_ban_member_confirmation_description), + content = stringResource(R.string.screen_bottom_sheet_manage_room_member_ban_member_confirmation_description), value = "", ) } is AsyncAction.Loading -> { LaunchedEffect(action) { - val userDisplayName = state.selectedRoomMember?.getBestName().orEmpty() + val userDisplayName = selectedRoomMember?.getBestName().orEmpty() asyncIndicatorState.enqueue { - AsyncIndicator.Loading(text = stringResource(CommonStrings.screen_bottom_sheet_manage_room_member_banning_user, userDisplayName)) + AsyncIndicator.Loading(text = stringResource(R.string.screen_bottom_sheet_manage_room_member_banning_user, userDisplayName)) } } } @@ -164,24 +160,21 @@ fun RoomMembersModerationView( } else -> Unit } - when (val action = state.unbanUserAsyncAction) { is AsyncAction.Confirming -> { - if (action is ConfirmingRoomMemberAction) { - 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), - onSubmitClick = { - val userDisplayName = action.roomMember.getBestName() - asyncIndicatorState.enqueue { - AsyncIndicator.Loading(text = stringResource(R.string.screen_room_member_list_unbanning_user, userDisplayName)) - } - state.eventSink(RoomMembersModerationEvents.UnbanUser(action.roomMember.userId)) - }, - onDismiss = { state.eventSink(RoomMembersModerationEvents.Reset) }, - ) - } + 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), + onSubmitClick = { + val userDisplayName = selectedRoomMember?.getBestName().orEmpty() + asyncIndicatorState.enqueue { + AsyncIndicator.Loading(text = stringResource(R.string.screen_room_member_list_unbanning_user, userDisplayName)) + } + state.eventSink(InternalRoomMemberModerationEvents.DoUnbanUser) + }, + onDismiss = { state.eventSink(InternalRoomMemberModerationEvents.Reset) }, + ) } is AsyncAction.Failure -> { Timber.e(action.error, "Failed to unban user.") @@ -260,7 +253,7 @@ private fun RoomMemberActionsBottomSheet( when (action) { is ModerationAction.DisplayProfile -> { ListItem( - headlineContent = { Text(stringResource(CommonStrings.screen_bottom_sheet_manage_room_member_member_user_info)) }, + headlineContent = { Text(stringResource(R.string.screen_bottom_sheet_manage_room_member_member_user_info)) }, leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Info())), onClick = { coroutineScope.launch { @@ -272,7 +265,7 @@ private fun RoomMemberActionsBottomSheet( } is ModerationAction.KickUser -> { ListItem( - headlineContent = { Text(stringResource(CommonStrings.screen_bottom_sheet_manage_room_member_remove)) }, + headlineContent = { Text(stringResource(R.string.screen_bottom_sheet_manage_room_member_remove)) }, leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Block())), onClick = { coroutineScope.launch { @@ -284,7 +277,7 @@ private fun RoomMemberActionsBottomSheet( } is ModerationAction.BanUser -> { ListItem( - headlineContent = { Text(stringResource(R.string.screen_room_member_list_manage_member_remove_confirmation_ban)) }, + headlineContent = { Text(stringResource(R.string.screen_bottom_sheet_manage_room_member_ban)) }, leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Block())), style = ListItemStyle.Destructive, onClick = { @@ -295,6 +288,7 @@ private fun RoomMemberActionsBottomSheet( } ) } + is ModerationAction.UnbanUser -> Unit } } } @@ -303,16 +297,16 @@ private fun RoomMemberActionsBottomSheet( @PreviewsDayNight @Composable -internal fun RoomMembersModerationViewPreview(@PreviewParameter(RoomMembersModerationStateProvider::class) state: RoomMembersModerationState) { +internal fun RoomMembersModerationViewPreview(@PreviewParameter(RoomMemberModerationStateProvider::class) state: InternalRoomMemberModerationState) { ElementPreview { Box( modifier = Modifier .fillMaxWidth() .heightIn(min = 64.dp) ) { - RoomMembersModerationView( + RoomMemberModerationView( state = state, - onDisplayMemberProfile = {}, + onSelectAction = {}, ) } } diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/di/RoomMemberModerationModule.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/di/RoomMemberModerationModule.kt new file mode 100644 index 0000000000..d8caadd71f --- /dev/null +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/di/RoomMemberModerationModule.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.roommembermoderation.impl.di + +import com.squareup.anvil.annotations.ContributesTo +import dagger.Binds +import dagger.Module +import io.element.android.features.roommembermoderation.api.RoomMemberModerationState +import io.element.android.features.roommembermoderation.impl.RoomMemberModerationPresenter +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.di.SessionScope + +@ContributesTo(RoomScope::class) +@Module +interface RoomMemberModerationModule { + @Binds + fun bindRoomMemberModerationPresenter(presenter: RoomMemberModerationPresenter): Presenter +} diff --git a/features/roommembermoderation/impl/src/main/res/values-be/translations.xml b/features/roommembermoderation/impl/src/main/res/values-be/translations.xml new file mode 100644 index 0000000000..8a488cf10c --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-be/translations.xml @@ -0,0 +1,18 @@ + + + "Выдаліць і заблакіраваць удзельніка" + "Заблакіраваць" + "Яны не змогуць зноў далучыцца да гэтага пакоя, калі іх запросяць." + "Вы ўпэўнены, што хочаце заблакіраваць гэтага карыстальніка?" + "Блакіроўка %1$s" + "Прагляд профілю" + "Выдаліць удзельніка з пакоя" + "Выдаліць удзельніка і забараніць далучацца ў будучыні?" + "Выдаленне %1$s…" + "Выдаліць і заблакіраваць удзельніка" + "Толькі выдаліць удзельніка" + "Разблакіраваць" + "Яны змогуць зноў далучыцца да гэтага пакоя, калі іх запросяць." + "Разблакіраваць удзельніка" + "Разблакіроўка %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-cs/translations.xml b/features/roommembermoderation/impl/src/main/res/values-cs/translations.xml new file mode 100644 index 0000000000..490671e9ff --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-cs/translations.xml @@ -0,0 +1,21 @@ + + + "Odebrat a vykázat člena" + "Vykázat" + "Nebudou se moci znovu připojit k této místnosti, pokud budou pozváni." + "Jste si jisti, že chcete vykázat tohoto člena?" + "Vykazování %1$s" + "Odebrat" + "Budou moci znovu vstoupit do této místnosti, pokud budou pozváni." + "Opravdu chcete tohoto člena odebrat?" + "Zobrazit profil" + "Odebrat z místnosti" + "Odebrat člena a zakázat mu připojení v budoucnu?" + "Odstraňování %1$s…" + "Odebrat a vykázat člena" + "Pouze odebrat člena" + "Zrušit vykázání" + "Pokud budou pozváni, budou se moci do této místnosti znovu připojit." + "Zrušit vykázání uživatele" + "Rušení vykázání %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-cy/translations.xml b/features/roommembermoderation/impl/src/main/res/values-cy/translations.xml new file mode 100644 index 0000000000..55fc68adf0 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-cy/translations.xml @@ -0,0 +1,21 @@ + + + "Gwahardd o ystafell" + "Atal" + "Fyddan nhw ddim yn gallu ymuno â\'r ystafell hon eto os cân nhw wahoddiad." + "Ydych chi\'n siŵr eich bod am wahardd yr aelod hwn?" + "Yn gwahardd %1$s" + "Tynnu" + "Fyddan nhw yn gallu ymuno â\'r ystafell hon eto os cân nhw wahoddiad." + "Ydych chi\'n siŵr eich bod am ddileu\'r aelod hwn?" + "Gweld proffil" + "Tynnu o\'r ystafell" + "Dileu aelod a\'u gwahardd rhag ymuno yn y dyfodol?" + "Wrthi\'n dileu %1$s…" + "Gwahardd o ystafell" + "Dileu aelod yn unig" + "Adfer" + "Fyddan nhw yn gallu ymuno â\'r ystafell hon eto os cân nhw wahoddiad." + "Gwahardd defnyddiwr" + "Dad-wahardd %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-de/translations.xml b/features/roommembermoderation/impl/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..c1fb73626d --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,21 @@ + + + "Mitglied entfernen und sperren" + "Sperren" + "Sie können dem Raum nicht mehr beitreten, selbst wenn sie eingeladen werden." + "Bist du sicher, dass du dieses Mitglied sperren möchtest?" + "%1$s wird gesperrt." + "Entfernen" + "Sie können diesen Raum wieder betreten, wenn sie eingeladen werden." + "Möchten Sie dieses Mitglied wirklich entfernen?" + "Benutzerinformationen anzeigen" + "Mitglied entfernen" + "Mitglied entfernen und den erneuten Beitritt sperren?" + "%1$s wird entfernt." + "Mitglied entfernen und sperren" + "Mitglied nur entfernen" + "Sperre aufheben" + "Die Nutzer können den Raum wieder beitreten, wenn sie dazu eingeladen werden." + "Benutzer entsperren" + "%1$s wird entsperrt." + diff --git a/features/roommembermoderation/impl/src/main/res/values-el/translations.xml b/features/roommembermoderation/impl/src/main/res/values-el/translations.xml new file mode 100644 index 0000000000..85ea09cae4 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-el/translations.xml @@ -0,0 +1,21 @@ + + + "Αφαίρεση και αποκλεισμός μέλους" + "Αποκλεισμός" + "Δεν θα μπορεί να συμμετέχει ξανά σε αυτό το δωμάτιο εάν προσκληθεί." + "Θες σίγουρα να αποκλείσεις αυτό το μέλος;" + "Αποκλεισμός %1$s" + "Αφαίρεση" + "Θα μπορούν να συμμετάσχουν ξανά σε αυτό το δωμάτιο εάν προσκληθούν." + "Είστε βέβαιοι ότι θέλετε να αφαιρέσετε αυτό το μέλος;" + "Προβολή προφίλ" + "Αφαίρεση από το δωμάτιο" + "Αφαίρεση μέλους και απαγόρευση συμμετοχής στο μέλλον;" + "Αφαίρεση %1$s…" + "Αφαίρεση και αποκλεισμός μέλους" + "Μόνο αφαίρεση μέλους" + "Αναίρεση αποκλεισμού" + "Θα μπορεί να συμμετάσχει ξανά στο δωμάτιο εάν προσκληθεί." + "Άρση αποκλεισμού χρήστη" + "Άρση αποκλεισμού %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-es/translations.xml b/features/roommembermoderation/impl/src/main/res/values-es/translations.xml new file mode 100644 index 0000000000..08a49a1425 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-es/translations.xml @@ -0,0 +1,18 @@ + + + "Sacar y vetar a un miembro" + "Vetar" + "No podrán volver a unirse a esta sala si son invitados." + "¿Estás seguro de que quieres vetar a este miembro?" + "Vetando a %1$s" + "Ver perfil" + "Sacar de la sala" + "¿Sacar al miembro y prohibirle unirse en el futuro?" + "Eliminando %1$s…" + "Sacar y vetar a un miembro" + "Solo eliminar miembro" + "Quitar veto" + "Podrán volver a unirse a esta sala si son invitados de nuevo." + "Quitar veto al usuario" + "Levantando veto a %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-et/translations.xml b/features/roommembermoderation/impl/src/main/res/values-et/translations.xml new file mode 100644 index 0000000000..98a05ea6b6 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-et/translations.xml @@ -0,0 +1,21 @@ + + + "Eemalda ja sea suhtluskeeld" + "Sea suhtluskeeld" + "Ta ei saa selle jututoaga liituda isegi kutse olemasolul." + "Kas sa oled kindel, et soovid sellele kasutajale seada suhtluskeelu?" + "Seame kasutajale %1$s suhtluskeelu" + "Eemalda" + "Uue kutse saamisel on tal võimalik selle jututoaga uuesti liituda." + "Kas sa oled kindel, et soovid selle osaleja eemaldada?" + "Vaata profiili" + "Eemalda kasutaja jututoast" + "Kas eemaldama kasutaja ja seame talle tulevikuks suhtluskeelu?" + "Eemaldame kasutajat %1$s…" + "Eemalda ja sea suhtluskeeld" + "Ainult eemalda kasutaja" + "Eemalda suhtluskeeld" + "Kutse olemasolul saab ta nüüd jututoaga uuesti liituda" + "Eemalda kasutaja suhtluskeeld" + "Eemaldame suhtluskeelu kasutajalt %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-eu/translations.xml b/features/roommembermoderation/impl/src/main/res/values-eu/translations.xml new file mode 100644 index 0000000000..c3d1f7c5a8 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-eu/translations.xml @@ -0,0 +1,16 @@ + + + "Kendu kidea eta ezarri debekua" + "Ezarri debekua" + "Ziur kide honi debekua ezarri nahi diozula?" + "%1$s(r)i debekua ezartzen" + "Ikusi profila" + "Kendu gelatik" + "Kidea kendu eta etorkizunean sartzea debekatu?" + "%1$s kentzen…" + "Kendu kidea eta ezarri debekua" + "Kendu kidea soilik" + "Kendu debekua" + "Kendu debekua erabiltzaileari" + "%1$s(r)i debekua kentzen" + diff --git a/features/roommembermoderation/impl/src/main/res/values-fa/translations.xml b/features/roommembermoderation/impl/src/main/res/values-fa/translations.xml new file mode 100644 index 0000000000..d1b986598a --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-fa/translations.xml @@ -0,0 +1,21 @@ + + + "برداشت و تحریم عضو" + "تحریم" + "در صورت دعوت نمی‌تواند دوباره به اتاق بپیوندد." + "مطمئنید می‌خواهید این عضو را تحریم کنید؟" + "تحریم کردن %1$s" + "برداشتن" + "در صورت دعوت می‌تواند دوباره به اتاق بپیوندد." + "مطمئنید می‌خواهید این عضو را بردارید؟" + "دیدن نمایه" + "برداشتن از اتاق" + "برداشتن عضو و تحریم پیوستن در آینده؟" + "برداشتن %1$s…" + "برداشت و تحریم عضو" + "تنها برداشتن عضو" + "رفع انسداد" + "در صورت دعوت می‌تواند دوباره به اتاق بپیوندد." + "تحریم نکردن کاربر" + "رفع تحریم %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-fi/translations.xml b/features/roommembermoderation/impl/src/main/res/values-fi/translations.xml new file mode 100644 index 0000000000..050d20600a --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-fi/translations.xml @@ -0,0 +1,21 @@ + + + "Poista jäsen huoneesta ja anna porttikielto" + "Anna porttikielto" + "He eivät voi enää liittyä tähän huoneeseen, jos heidät kutsutaan." + "Haluatko varmasti antaa tälle jäsenelle porttikiellon?" + "Annetaan porttikieltoa käyttäjälle %1$s" + "Poista" + "He voivat liittyä tähän huoneeseen uudelleen, jos heidät kutsutaan." + "Haluatko varmasti poistaa tämän jäsenen?" + "Näytä profiili" + "Poista huoneesta" + "Poistetaanko jäsen huoneesta ja kielletäänkö heitä liittymästä tulevaisuudessa?" + "Poistetaan käyttäjää %1$s huoneesta…" + "Poista jäsen huoneesta ja anna porttikielto" + "Poista vain jäsen huoneesta" + "Poista porttikielto" + "He voivat liittyä tähän huoneeseen uudelleen, jos heidät kutsutaan." + "Poista käyttäjän porttikielto" + "Poistetaan käyttäjän %1$s porttikieltoa" + diff --git a/features/roommembermoderation/impl/src/main/res/values-fr/translations.xml b/features/roommembermoderation/impl/src/main/res/values-fr/translations.xml new file mode 100644 index 0000000000..88cfd7b309 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-fr/translations.xml @@ -0,0 +1,21 @@ + + + "Retirer et bannir ce membre" + "Bannir" + "Il ne pourra pas rejoindre le salon à nouveau, même si il est invité." + "Êtes-vous certain de vouloir bannir ce membre ?" + "Bannissement de %1$s" + "Retirer" + "Cet utilisateur pourra rejoindre le salon à nouveau si il est invité." + "Voulez-vous vraiment supprimer ce membre ?" + "Voir le profil" + "Retirer le membre du salon" + "Retirer le membre et interdire l’adhésion à l’avenir ?" + "Enlever %1$s…" + "Retirer et bannir ce membre" + "Retirer le membre uniquement" + "Débannir" + "Il pourra rejoindre le salon à nouveau si il est invité." + "Débannir l’utilisateur" + "Débannissement de %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-hu/translations.xml b/features/roommembermoderation/impl/src/main/res/values-hu/translations.xml new file mode 100644 index 0000000000..bb2123e479 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-hu/translations.xml @@ -0,0 +1,21 @@ + + + "Eltávolítás és a tag kitiltása" + "Kitiltás" + "Többé nem csatlakozhat ehhez a szobához, akkor sem, ha meghívják." + "Biztos, hogy kitiltja ezt a tagot?" + "%1$s kitiltása" + "Eltávolítás" + "Ehhez a szobához is csatlakozhat, ha meghívják." + "Biztos, hogy eltávolítja ezt a tagot?" + "Profil megtekintése" + "Eltávolítás a szobából" + "Eltávolítja a tagot, és megtiltja a jövőbeni csatlakozást?" + "%1$s eltávolítása…" + "Eltávolítás és a tag kitiltása" + "Csak a tag eltávolítása" + "Tiltás feloldása" + "Ehhez a szobához is csatlakozhat, ha meghívják." + "Felhasználó tiltásának feloldása" + "%1$s tiltásának feloldása" + diff --git a/features/roommembermoderation/impl/src/main/res/values-it/translations.xml b/features/roommembermoderation/impl/src/main/res/values-it/translations.xml new file mode 100644 index 0000000000..a1bdeed0b0 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-it/translations.xml @@ -0,0 +1,18 @@ + + + "Rimuovi ed escludi" + "Escludi" + "Non potrà entrare nuovamente in questa stanza se invitato." + "Vuoi davvero escludere questo membro?" + "Esclusione di %1$s" + "Visualizza profilo" + "Rimuovi dalla stanza" + "Rimuovere e vietare l\'accesso in futuro?" + "Rimozione di %1$s…" + "Rimuovi ed escludi" + "Rimuovi soltanto" + "Riammetti" + "Potrà entrare nuovamente in questa stanza se invitato." + "Riammetti utente" + "Riammissione di %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-ka/translations.xml b/features/roommembermoderation/impl/src/main/res/values-ka/translations.xml new file mode 100644 index 0000000000..730fff2714 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-ka/translations.xml @@ -0,0 +1,18 @@ + + + "წევრის წაშლა და დაბლოკვა" + "დაბლოკვა" + "მოწვევის შემთხვევაში ამ ოთახში კვლავ გაწევრიანებას ვერ შეძლებენ." + "დარწმუნებული ხართ, რომ ამ წევრის დაბლოკვა გსურთ?" + "%1$s-ს დაბლოკვა" + "პროფილის ნახვა" + "ოთახიდან გაგდება" + "გსურთ წევრის გაგდება და მომავალში გაწევრიანების აკრძალვა?" + "%1$s-ს გაგდება…" + "წევრის წაშლა და დაბლოკვა" + "მხოლოდ წევრის წაშლა" + "განბლოკვა" + "მოწვევის შემთხვევაში განბლოკილი მომხმარებელი ისევ შეძლებს ოთახს შეუერთდეს." + "მომხმარებლის განბლოკვა" + "%1$s-ს განბლოკვა" + diff --git a/features/roommembermoderation/impl/src/main/res/values-nb/translations.xml b/features/roommembermoderation/impl/src/main/res/values-nb/translations.xml new file mode 100644 index 0000000000..b25be530ec --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-nb/translations.xml @@ -0,0 +1,21 @@ + + + "Fjern og utesteng medlem" + "Utesteng" + "De vil ikke kunne bli med i dette rommet igjen hvis de blir invitert." + "Er du sikker på at du vil utestenge dette medlemmet?" + "Utestenger %1$s" + "Fjern" + "De vil kunne bli med i dette rommet igjen hvis de blir invitert." + "Er du sikker på at du vil fjerne dette medlemmet?" + "Vis profil" + "Fjern fra rommet" + "Fjerne medlem og utestenge fra å bli med i fremtiden?" + "Fjerner %1$s…" + "Fjern og utesteng medlem" + "Bare fjern medlem" + "Opphev utestengelse" + "De vil kunne bli med i dette rommet igjen hvis de blir invitert." + "Opphev utestengelse av bruker" + "Oppheve utestengelsen av %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-nl/translations.xml b/features/roommembermoderation/impl/src/main/res/values-nl/translations.xml new file mode 100644 index 0000000000..f025cbb584 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-nl/translations.xml @@ -0,0 +1,18 @@ + + + "Lid verwijderen en verbannen" + "Verbannen" + "Ze kunnen niet meer toetreden tot deze kamer als ze worden uitgenodigd." + "Weet je zeker dat je dit lid wilt verbannen?" + "%1$s verbannen" + "Profiel bekijken" + "Verwijderen uit kamer" + "Lid verwijderen en toekomstige deelname verbieden?" + "%1$s wordt verwijderd…" + "Lid verwijderen en verbannen" + "Alleen lid verwijderen" + "Ontbannen" + "Ze kunnen opnieuw tot de kamer toetreden als ze worden uitgenodigd." + "Ontban gebruiker" + "%1$s ontbannen" + diff --git a/features/roommembermoderation/impl/src/main/res/values-pl/translations.xml b/features/roommembermoderation/impl/src/main/res/values-pl/translations.xml new file mode 100644 index 0000000000..40bd309913 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-pl/translations.xml @@ -0,0 +1,21 @@ + + + "Usuń i zbanuj członka" + "Zbanuj" + "Nie będą mogli ponownie dołączyć do tego pokoju, jeśli zostaną zaproszeni." + "Czy na pewno chcesz zbanować tego członka?" + "Banowanie %1$s" + "Usuń" + "Będą mogli ponownie dołączyć do pokoju, jeśli zostaną zaproszeni." + "Czy na pewno chcesz usunąć tego członka?" + "Wyświetl profil" + "Usuń z pokoju" + "Usunąć członka i zablokować możliwość dołączenia w przyszłości?" + "Usuwanie %1$s…" + "Usuń i zbanuj członka" + "Tylko usuń członka" + "Odbanuj" + "Będą mogli ponownie dołączyć do tego pokoju, jeśli zostaną zaproszeni." + "Odbanuj użytkownika" + "Odbanowanie %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-pt-rBR/translations.xml b/features/roommembermoderation/impl/src/main/res/values-pt-rBR/translations.xml new file mode 100644 index 0000000000..764c313a96 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-pt-rBR/translations.xml @@ -0,0 +1,18 @@ + + + "Remover e banir membro" + "Banir" + "Eles não poderão entrar nesta sala novamente se forem convidados." + "Tem certeza de que quer banir este membro?" + "Banindo %1$s" + "Ver perfil" + "Remover da sala" + "Remover membro e banir de entrar novamente no futuro?" + "Removendo %1$s…" + "Remover e banir membro" + "Somente remover membro" + "Desbanir" + "Eles poderão entrar nesta sala novamente se forem convidados." + "Desbanir usuário" + "Desbanindo %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-pt/translations.xml b/features/roommembermoderation/impl/src/main/res/values-pt/translations.xml new file mode 100644 index 0000000000..89ddd6f5ba --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-pt/translations.xml @@ -0,0 +1,19 @@ + + + "Remover e banir participante" + "Banir" + "Não poderão voltar a entrar nesta sala, mesmo se forem convidados." + "Tens a certeza que queres banir este participante?" + "A banir %1$s" + "Poderão entrar na sala novamente se convidados." + "Ver perfil" + "Remover da sala" + "Remover participante e proibir que entre no futuro?" + "A remover %1$s…" + "Remover e banir participante" + "Remover apenas" + "Anular banimento" + "Poderão juntar-se novamente a esta sala se forem convidados." + "Anular banimento do utilizador" + "A anular banimento de %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-ro/translations.xml b/features/roommembermoderation/impl/src/main/res/values-ro/translations.xml new file mode 100644 index 0000000000..526db044db --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-ro/translations.xml @@ -0,0 +1,18 @@ + + + "Eliminați și interziceți membrul" + "Interzicere" + "Nu se vor putea alătura din nou acestei camere dacă sunt invitați." + "Sunteți sigur că doriți să interziceți acest membru?" + "Se interzice %1$s" + "Vizualizare profil" + "Înlăturați membrul" + "Înlăturați membrul și interziceți-i să se alăture în viitor?" + "Se elimină %1$s" + "Eliminați și interziceți membrul" + "Doar înlăturare" + "Anulare excludere" + "Se vor putea alătura din nou acestei săli dacă sunt invitați." + "Anulați interzicerea utilizatorului" + "Se anulează interzicerea lui %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-ru/translations.xml b/features/roommembermoderation/impl/src/main/res/values-ru/translations.xml new file mode 100644 index 0000000000..93fa22ac3f --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-ru/translations.xml @@ -0,0 +1,20 @@ + + + "Удалить и заблокировать участника" + "Заблокировать" + "Они не смогут снова присоединиться к этой комнате, если их пригласят." + "Вы уверены, что хотите заблокировать этого участника?" + "Блокировка %1$s" + "Удалить" + "Вы действительно хотите удалить этого участника?" + "Посмотреть профиль" + "Удалить участника из комнаты" + "Удалить участника и запретить присоединяться в будущем?" + "Удаление %1$s…" + "Удалить и заблокировать участника" + "Только удалить участника" + "Разблокировать" + "Они снова смогут присоединиться в эту комнату если их пригласят." + "Разбанить пользователя?" + "Разблокировка %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-sk/translations.xml b/features/roommembermoderation/impl/src/main/res/values-sk/translations.xml new file mode 100644 index 0000000000..8e7c4a1399 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-sk/translations.xml @@ -0,0 +1,21 @@ + + + "Odstrániť a zakázať člena" + "Zakázať" + "Nebudú sa môcť pripojiť k tejto miestnosti znova ani ak budú pozvaní." + "Ste si istý, že chcete zakázať tohto člena?" + "Zakazuje sa %1$s" + "Odstrániť" + "V prípade pozvania sa budú môcť znova pripojiť k tejto miestnosti." + "Ste si istý, že chcete odstrániť tohto člena?" + "Zobraziť profil" + "Odstrániť z miestnosti" + "Odstrániť člena a zakázať vstup v budúcnosti?" + "Odstraňuje sa %1$s…" + "Odstrániť a zakázať člena" + "Iba odstrániť člena" + "Zrušiť zákaz" + "V prípade pozvania sa budú môcť znova pripojiť k tejto miestnosti." + "Zrušiť zákaz používateľa" + "Zrušenie zákazu %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-sv/translations.xml b/features/roommembermoderation/impl/src/main/res/values-sv/translations.xml new file mode 100644 index 0000000000..bb512b1142 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-sv/translations.xml @@ -0,0 +1,21 @@ + + + "Ta bort och banna medlem" + "Banna" + "Denne kommer inte att kunna gå med i det här rummet igen om denne bjuds in." + "Är du säker på att du vill banna den här medlemmen?" + "Bannar %1$s" + "Ta bort" + "Denne kommer kunna gå med i rummet igen om denne bjuds in" + "Är du säker på att du vill ta bort den här medlemmen?" + "Visa profil" + "Ta bort från rummet" + "Ta bort medlem och banna från att gå med i framtiden?" + "Tar bort %1$s …" + "Ta bort och banna medlem" + "Ta bara bort medlem" + "Avbanna" + "Denne kommer kunna gå med i rummet igen om denne bjuds in" + "Avbanna användare" + "Avbannar %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-tr/translations.xml b/features/roommembermoderation/impl/src/main/res/values-tr/translations.xml new file mode 100644 index 0000000000..beee8fa720 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-tr/translations.xml @@ -0,0 +1,18 @@ + + + "Üyeyi çıkar ve yasakla" + "Yasakla" + "Davet edilseler bile bu odaya tekrar katılamazlar." + "Bu üyeyi yasaklamak istediğinize emin misiniz?" + "Yasaklanıyor %1$s" + "Profili görüntüle" + "Odadan çıkar" + "Üyeyi çıkarın ve gelecekte katılmasını yasaklayın?" + "Kaldırılıyor %1$s…" + "Üyeyi çıkar ve yasakla" + "Yalnızca üyeyi kaldır" + "Yasağı Kaldır" + "Davet edildikleri takdirde bu odaya tekrar katılabileceklerdir." + "Kullanıcının yasağını kaldır" + "Yasak kaldırılıyor %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-uk/translations.xml b/features/roommembermoderation/impl/src/main/res/values-uk/translations.xml new file mode 100644 index 0000000000..d6ca0625a8 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-uk/translations.xml @@ -0,0 +1,21 @@ + + + "Вилучити й заблокувати учасника" + "Заблокувати" + "Він не зможе приєднатися до цієї кімнати знову, якщо його запросять." + "Ви точно хочете заблокувати цього користувача?" + "Блокування %1$s" + "Вилучити" + "Вони зможуть знову приєднатися до цієї кімнати, якщо їх запросять." + "Ви дійсно хочете вилучити цього учасника?" + "Переглянути профіль" + "Вилучити з кімнати" + "Вилучити учасника та заборонити приєднання в майбутньому?" + "Вилучення %1$s…" + "Вилучити й заблокувати учасника" + "Лише вилучити учасника" + "Розблокувати" + "Вони зможуть знову приєднатися до цієї кімнати, якщо їх запросять." + "Розблокувати користувача" + "Розблокування %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-zh-rTW/translations.xml b/features/roommembermoderation/impl/src/main/res/values-zh-rTW/translations.xml new file mode 100644 index 0000000000..6f1ab7d548 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-zh-rTW/translations.xml @@ -0,0 +1,21 @@ + + + "踢出並加入黑名單" + "加入黑名單" + "即使收到邀請,他們仍然無法加入聊天室。" + "您確定要將此成員加入黑名單?" + "正在將 %1$s 加入黑名單" + "移除" + "若收到邀請,他們可以再次加入此聊天室。" + "您真的想要移除此成員嗎?" + "查看個人檔案" + "踢出聊天室" + "移除成員並禁止未來再度加入?" + "正在踢出 %1$s…" + "踢出並加入黑名單" + "僅移除成員" + "解除黑名單" + "如果收到邀請,他們能再次加入聊天室。" + "解除黑名單" + "正在解除黑名單 %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-zh/translations.xml b/features/roommembermoderation/impl/src/main/res/values-zh/translations.xml new file mode 100644 index 0000000000..cf89b13e06 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-zh/translations.xml @@ -0,0 +1,21 @@ + + + "移除并封禁成员" + "封禁" + "即使受到邀请,他们也无法再次加入聊天室。" + "您确定要封禁该成员吗?" + "封禁 %1$s" + "移除" + "如果受到邀请,他们可以重新加入聊天室。" + "您确定要移除此成员吗?" + "查看个人资料" + "从聊天室移除" + "删除成员并禁止重新加入?" + "正在移除 %1$s……" + "移除并封禁成员" + "仅移除成员" + "取消封禁" + "如果受到邀请,他们可以重新加入聊天室。" + "解封用户" + "解除封禁 %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values/localazy.xml b/features/roommembermoderation/impl/src/main/res/values/localazy.xml new file mode 100644 index 0000000000..8e9b3defc1 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values/localazy.xml @@ -0,0 +1,21 @@ + + + "Ban from room" + "Ban" + "They won’t be able to join this room again if invited." + "Are you sure you want to ban this member?" + "Banning %1$s" + "Remove" + "They will be able to join this room again if invited." + "Are you sure you want to remove this member?" + "View profile" + "Remove from room" + "Remove member and ban from joining in the future?" + "Removing %1$s…" + "Ban from room" + "Only remove member" + "Unban" + "They will be able to join this room again if invited." + "Unban user" + "Unbanning %1$s" + diff --git a/libraries/ui-strings/src/main/res/values-be/translations.xml b/libraries/ui-strings/src/main/res/values-be/translations.xml index 6448bfb974..51a60d788f 100644 --- a/libraries/ui-strings/src/main/res/values-be/translations.xml +++ b/libraries/ui-strings/src/main/res/values-be/translations.xml @@ -289,15 +289,6 @@ "Гэй, пагавары са мной у %1$s: %2$s" "%1$s Android" "Паведаміць аб памылцы з дапамогай Rageshake" - "Выдаліць і заблакіраваць удзельніка" - "Заблакіраваць" - "Яны не змогуць зноў далучыцца да гэтага пакоя, калі іх запросяць." - "Вы ўпэўнены, што хочаце заблакіраваць гэтага карыстальніка?" - "Блакіроўка %1$s" - "Прагляд профілю" - "Выдаліць удзельніка з пакоя" - "Выдаліць удзельніка і забараніць далучацца ў будучыні?" - "Выдаленне %1$s…" "Не ўдалося выбраць носьбіт, паўтарыце спробу." "Не атрымалася апрацаваць медыяфайл для загрузкі, паспрабуйце яшчэ раз." "Не атрымалася загрузіць медыяфайлы, паспрабуйце яшчэ раз." diff --git a/libraries/ui-strings/src/main/res/values-cs/translations.xml b/libraries/ui-strings/src/main/res/values-cs/translations.xml index 4d7409e7c1..01bb2d1519 100644 --- a/libraries/ui-strings/src/main/res/values-cs/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml @@ -343,18 +343,6 @@ Opravdu chcete pokračovat?" "Ahoj, ozvi se mi na %1$s: %2$s" "%1$s Android" "Zatřeste zařízením pro nahlášení chyby" - "Odebrat a vykázat člena" - "Vykázat" - "Nebudou se moci znovu připojit k této místnosti, pokud budou pozváni." - "Jste si jisti, že chcete vykázat tohoto člena?" - "Vykazování %1$s" - "Odebrat" - "Budou moci znovu vstoupit do této místnosti, pokud budou pozváni." - "Opravdu chcete tohoto člena odebrat?" - "Zobrazit profil" - "Odebrat z místnosti" - "Odebrat člena a zakázat mu připojení v budoucnu?" - "Odstraňování %1$s…" "Výběr média se nezdařil, zkuste to prosím znovu." "Titulky nemusí být viditelné pro lidi, kteří používají starší aplikace." "Nahrání média se nezdařilo, zkuste to prosím znovu." diff --git a/libraries/ui-strings/src/main/res/values-cy/translations.xml b/libraries/ui-strings/src/main/res/values-cy/translations.xml index 262e145296..6cda9da5ff 100644 --- a/libraries/ui-strings/src/main/res/values-cy/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cy/translations.xml @@ -355,18 +355,6 @@ Ydych chi\'n siŵr eich bod am barhau?" "Hei, siaradwch â mi ar %1$s: %2$s" "Android %1$s" "Rageshake i adrodd gwall" - "Gwahardd o ystafell" - "Atal" - "Fyddan nhw ddim yn gallu ymuno â\'r ystafell hon eto os cân nhw wahoddiad." - "Ydych chi\'n siŵr eich bod am wahardd yr aelod hwn?" - "Yn gwahardd %1$s" - "Tynnu" - "Fyddan nhw yn gallu ymuno â\'r ystafell hon eto os cân nhw wahoddiad." - "Ydych chi\'n siŵr eich bod am ddileu\'r aelod hwn?" - "Gweld proffil" - "Tynnu o\'r ystafell" - "Dileu aelod a\'u gwahardd rhag ymuno yn y dyfodol?" - "Wrthi\'n dileu %1$s…" "Wedi methu dewis cyfrwng, ceisiwch eto." "Efallai na fydd capsiynau yn weladwy i bobl sy\'n defnyddio apiau hŷn." "Wedi methu â phrosesu cyfryngau i\'w llwytho, ceisiwch eto." diff --git a/libraries/ui-strings/src/main/res/values-de/translations.xml b/libraries/ui-strings/src/main/res/values-de/translations.xml index 8945b8cd6b..451d4bc140 100644 --- a/libraries/ui-strings/src/main/res/values-de/translations.xml +++ b/libraries/ui-strings/src/main/res/values-de/translations.xml @@ -339,18 +339,6 @@ Möchten Sie wirklich fortfahren?" "Hey, sprich mit mir auf %1$s: %2$s" "%1$s Android" "Schüttel heftig zum Melden von Fehlern" - "Mitglied entfernen und sperren" - "Sperren" - "Sie können dem Raum nicht mehr beitreten, selbst wenn sie eingeladen werden." - "Bist du sicher, dass du dieses Mitglied sperren möchtest?" - "%1$s wird gesperrt." - "Entfernen" - "Sie können diesen Raum wieder betreten, wenn sie eingeladen werden." - "Möchten Sie dieses Mitglied wirklich entfernen?" - "Benutzerinformationen anzeigen" - "Mitglied entfernen" - "Mitglied entfernen und den erneuten Beitritt sperren?" - "%1$s wird entfernt." "Medienauswahl fehlgeschlagen, bitte versuche es erneut." "Bildunterschriften sind für Nutzer älterer Apps möglicherweise nicht sichtbar." "Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuche es erneut." diff --git a/libraries/ui-strings/src/main/res/values-el/translations.xml b/libraries/ui-strings/src/main/res/values-el/translations.xml index 24a92cad85..7b7c4ea183 100644 --- a/libraries/ui-strings/src/main/res/values-el/translations.xml +++ b/libraries/ui-strings/src/main/res/values-el/translations.xml @@ -339,18 +339,6 @@ "Γεια, μίλα μου στην εφαρμογή %1$s :%2$s" "%1$s Android" "Κούνησε δυνατά τη συσκευή σου για να αναφέρεις κάποιο σφάλμα" - "Αφαίρεση και αποκλεισμός μέλους" - "Αποκλεισμός" - "Δεν θα μπορεί να συμμετέχει ξανά σε αυτό το δωμάτιο εάν προσκληθεί." - "Θες σίγουρα να αποκλείσεις αυτό το μέλος;" - "Αποκλεισμός %1$s" - "Αφαίρεση" - "Θα μπορούν να συμμετάσχουν ξανά σε αυτό το δωμάτιο εάν προσκληθούν." - "Είστε βέβαιοι ότι θέλετε να αφαιρέσετε αυτό το μέλος;" - "Προβολή προφίλ" - "Αφαίρεση από το δωμάτιο" - "Αφαίρεση μέλους και απαγόρευση συμμετοχής στο μέλλον;" - "Αφαίρεση %1$s…" "Αποτυχία επιλογής πολυμέσου, δοκίμασε ξανά." "Οι λεζάντες ενδέχεται να μην είναι ορατές σε άτομα που χρησιμοποιούν παλαιότερες εφαρμογές." "Αποτυχία μεταφόρτωσης μέσου, δοκίμασε ξανά." diff --git a/libraries/ui-strings/src/main/res/values-es/translations.xml b/libraries/ui-strings/src/main/res/values-es/translations.xml index 1cf59dacbf..66ffa2aee3 100644 --- a/libraries/ui-strings/src/main/res/values-es/translations.xml +++ b/libraries/ui-strings/src/main/res/values-es/translations.xml @@ -316,15 +316,6 @@ Motivo: %1$s." "Hola, puedes hablar conmigo en %1$s: %2$s" "%1$s Android" "Agitar con fuerza para informar de un error" - "Sacar y vetar a un miembro" - "Vetar" - "No podrán volver a unirse a esta sala si son invitados." - "¿Estás seguro de que quieres vetar a este miembro?" - "Vetando a %1$s" - "Ver perfil" - "Sacar de la sala" - "¿Sacar al miembro y prohibirle unirse en el futuro?" - "Eliminando %1$s…" "Error al seleccionar archivos multimedia, por favor inténtalo de nuevo." "Es posible que las leyendas no sean visibles para las personas que usan aplicaciones más antiguas." "Error al procesar el contenido multimedia, por favor inténtalo de nuevo." diff --git a/libraries/ui-strings/src/main/res/values-et/translations.xml b/libraries/ui-strings/src/main/res/values-et/translations.xml index 4d0bc5c422..e1e7f809c6 100644 --- a/libraries/ui-strings/src/main/res/values-et/translations.xml +++ b/libraries/ui-strings/src/main/res/values-et/translations.xml @@ -339,18 +339,6 @@ Kas sa oled kindel, et soovid jätkata?" "Hei, suhtle minuga %1$s võrgus: %2$s" "%1$s Android" "Veast teatamiseks raputa nutiseadet ägedalt" - "Eemalda ja sea suhtluskeeld" - "Sea suhtluskeeld" - "Ta ei saa selle jututoaga liituda isegi kutse olemasolul." - "Kas sa oled kindel, et soovid sellele kasutajale seada suhtluskeelu?" - "Seame kasutajale %1$s suhtluskeelu" - "Eemalda" - "Uue kutse saamisel on tal võimalik selle jututoaga uuesti liituda." - "Kas sa oled kindel, et soovid selle osaleja eemaldada?" - "Vaata profiili" - "Eemalda kasutaja jututoast" - "Kas eemaldama kasutaja ja seame talle tulevikuks suhtluskeelu?" - "Eemaldame kasutajat %1$s…" "Meediafaili valimine ei õnnestunud. Palun proovi uuesti." "Selgitused ja alapealkirjad ei pruugi olla nähtavad vanemate rakenduste kasutajatele." "Meediafaili töötlemine enne üleslaadimist ei õnnestunud. Palun proovi uuesti." diff --git a/libraries/ui-strings/src/main/res/values-eu/translations.xml b/libraries/ui-strings/src/main/res/values-eu/translations.xml index e43f2d24f7..73d10a27e1 100644 --- a/libraries/ui-strings/src/main/res/values-eu/translations.xml +++ b/libraries/ui-strings/src/main/res/values-eu/translations.xml @@ -312,14 +312,6 @@ Arrazoia: %1$s." "🔐️ Zatoz nirekin %1$s(e)ra" "%1$s Android" "Astindu erroreen berri emateko" - "Kendu kidea eta ezarri debekua" - "Ezarri debekua" - "Ziur kide honi debekua ezarri nahi diozula?" - "%1$s(r)i debekua ezartzen" - "Ikusi profila" - "Kendu gelatik" - "Kidea kendu eta etorkizunean sartzea debekatu?" - "%1$s kentzen…" "Huts egin du multimedia aukeratzeak, saiatu berriro." "Huts egin du multimedia igotzeak, saiatu berriro." "Finkatutako mezuak" diff --git a/libraries/ui-strings/src/main/res/values-fa/translations.xml b/libraries/ui-strings/src/main/res/values-fa/translations.xml index 50f98c1a19..4fb5ceea2d 100644 --- a/libraries/ui-strings/src/main/res/values-fa/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fa/translations.xml @@ -310,18 +310,6 @@ "🔐️ پییوستن به من روی %1$s" "درود. با من روی %1$s صحبت کن: %2$s" "%1$s اندروید" - "برداشت و تحریم عضو" - "تحریم" - "در صورت دعوت نمی‌تواند دوباره به اتاق بپیوندد." - "مطمئنید می‌خواهید این عضو را تحریم کنید؟" - "تحریم کردن %1$s" - "برداشتن" - "در صورت دعوت می‌تواند دوباره به اتاق بپیوندد." - "مطمئنید می‌خواهید این عضو را بردارید؟" - "دیدن نمایه" - "برداشتن از اتاق" - "برداشتن عضو و تحریم پیوستن در آینده؟" - "برداشتن %1$s…" "گزینش رسانه شکست خورد. لطفاً دوباره تلاش کنید." "پردازش رسانه برای بارگذاری شکست خورد. لطفاً دوباره تلاش کنید." "بارگذاری رسانه شکست خورد. لطفاً دوباره تلاش کنید." diff --git a/libraries/ui-strings/src/main/res/values-fi/translations.xml b/libraries/ui-strings/src/main/res/values-fi/translations.xml index 293b3a49e2..5dd5b6b3b2 100644 --- a/libraries/ui-strings/src/main/res/values-fi/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fi/translations.xml @@ -339,18 +339,6 @@ Haluatko varmasti jatkaa?" "Hei, keskustele kanssani %1$s -sovelluksessa: %2$s" "%1$s Android" "Raivostunut ravistaminen ilmoittaa virheestä" - "Poista jäsen huoneesta ja anna porttikielto" - "Anna porttikielto" - "He eivät voi enää liittyä tähän huoneeseen, jos heidät kutsutaan." - "Haluatko varmasti antaa tälle jäsenelle porttikiellon?" - "Annetaan porttikieltoa käyttäjälle %1$s" - "Poista" - "He voivat liittyä tähän huoneeseen uudelleen, jos heidät kutsutaan." - "Haluatko varmasti poistaa tämän jäsenen?" - "Näytä profiili" - "Poista huoneesta" - "Poistetaanko jäsen huoneesta ja kielletäänkö heitä liittymästä tulevaisuudessa?" - "Poistetaan käyttäjää %1$s huoneesta…" "Median valinta epäonnistui, yritä uudelleen." "Kuvatekstit eivät välttämättä näy ihmisille, jotka käyttävät vanhempia sovelluksia." "Median käsittely epäonnistui, yritä uudelleen." diff --git a/libraries/ui-strings/src/main/res/values-fr/translations.xml b/libraries/ui-strings/src/main/res/values-fr/translations.xml index 27cac31aa0..1171f32df8 100644 --- a/libraries/ui-strings/src/main/res/values-fr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fr/translations.xml @@ -339,18 +339,6 @@ Raison : %1$s." "Salut, parle-moi sur %1$s : %2$s" "%1$s Android" "Rageshake pour signaler un problème" - "Retirer et bannir ce membre" - "Bannir" - "Il ne pourra pas rejoindre le salon à nouveau, même si il est invité." - "Êtes-vous certain de vouloir bannir ce membre ?" - "Bannissement de %1$s" - "Retirer" - "Cet utilisateur pourra rejoindre le salon à nouveau si il est invité." - "Voulez-vous vraiment supprimer ce membre ?" - "Voir le profil" - "Retirer le membre du salon" - "Retirer le membre et interdire l’adhésion à l’avenir ?" - "Enlever %1$s…" "Échec de la sélection du média, veuillez réessayer." "Les légendes peuvent ne pas être visibles pour les utilisateurs d’anciennes applications." "Échec du traitement des médias à télécharger, veuillez réessayer." diff --git a/libraries/ui-strings/src/main/res/values-hu/translations.xml b/libraries/ui-strings/src/main/res/values-hu/translations.xml index d304030e00..2704f8796b 100644 --- a/libraries/ui-strings/src/main/res/values-hu/translations.xml +++ b/libraries/ui-strings/src/main/res/values-hu/translations.xml @@ -339,18 +339,6 @@ Biztos, hogy folytatja?" "Beszélgessünk itt: %1$s, %2$s" "%1$s Android" "Az eszköz rázása a hibajelentéshez" - "Eltávolítás és a tag kitiltása" - "Kitiltás" - "Többé nem csatlakozhat ehhez a szobához, akkor sem, ha meghívják." - "Biztos, hogy kitiltja ezt a tagot?" - "%1$s kitiltása" - "Eltávolítás" - "Ehhez a szobához is csatlakozhat, ha meghívják." - "Biztos, hogy eltávolítja ezt a tagot?" - "Profil megtekintése" - "Eltávolítás a szobából" - "Eltávolítja a tagot, és megtiltja a jövőbeni csatlakozást?" - "%1$s eltávolítása…" "Nem sikerült kiválasztani a médiát, próbálja újra." "Előfordulhat, hogy a feliratok nem láthatók a régebbi alkalmazásokat használók számára." "Nem sikerült feldolgozni a feltöltendő médiát, próbálja újra." diff --git a/libraries/ui-strings/src/main/res/values-in/translations.xml b/libraries/ui-strings/src/main/res/values-in/translations.xml index c32ce5f730..cec28f2ad4 100644 --- a/libraries/ui-strings/src/main/res/values-in/translations.xml +++ b/libraries/ui-strings/src/main/res/values-in/translations.xml @@ -304,15 +304,6 @@ Alasan: %1$s." "Hai, bicaralah dengan saya di %1$s: %2$s" "%1$s Android" "Rageshake untuk melaporkan kutu" - "Keluarkan dan cekal anggota" - "Cekal" - "Mereka tidak akan dapat bergabung ke ruangan ini lagi jika diundang." - "Apakah Anda yakin ingin mencekal anggota ini?" - "Mencekal %1$s" - "Tampilkan profil" - "Keluarkan dari ruangan" - "Keluarkan pengguna dan cekal pengguna bergabung lagi di masa mendatang?" - "Mengeluarkan %1$s…" "Gagal memilih media, silakan coba lagi." "Keterangan mungkin tidak terlihat oleh orang yang menggunakan aplikasi lama." "Gagal memproses media untuk diunggah, silakan coba lagi." diff --git a/libraries/ui-strings/src/main/res/values-it/translations.xml b/libraries/ui-strings/src/main/res/values-it/translations.xml index e9864e090d..bf11478b9e 100644 --- a/libraries/ui-strings/src/main/res/values-it/translations.xml +++ b/libraries/ui-strings/src/main/res/values-it/translations.xml @@ -326,15 +326,6 @@ Sei sicuro di voler continuare?" "Ehi, parliamo su %1$s: %2$s" "%1$s Android" "Scuoti per segnalare un problema" - "Rimuovi ed escludi" - "Escludi" - "Non potrà entrare nuovamente in questa stanza se invitato." - "Vuoi davvero escludere questo membro?" - "Esclusione di %1$s" - "Visualizza profilo" - "Rimuovi dalla stanza" - "Rimuovere e vietare l\'accesso in futuro?" - "Rimozione di %1$s…" "Selezione del file multimediale fallita, riprova." "Le didascalie potrebbero non essere visibili agli utenti di app meno recenti." "Elaborazione del file multimediale da caricare fallita, riprova." diff --git a/libraries/ui-strings/src/main/res/values-ka/translations.xml b/libraries/ui-strings/src/main/res/values-ka/translations.xml index 0405aa6235..88bd672c82 100644 --- a/libraries/ui-strings/src/main/res/values-ka/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ka/translations.xml @@ -247,15 +247,6 @@ "გაგიმარჯოს! მესაუბრე %1$s-ზე: %2$s" "%1$s Android" "შეცდომის შესატყობინებლად ტელეფონის შენჯღრევა" - "წევრის წაშლა და დაბლოკვა" - "დაბლოკვა" - "მოწვევის შემთხვევაში ამ ოთახში კვლავ გაწევრიანებას ვერ შეძლებენ." - "დარწმუნებული ხართ, რომ ამ წევრის დაბლოკვა გსურთ?" - "%1$s-ს დაბლოკვა" - "პროფილის ნახვა" - "ოთახიდან გაგდება" - "გსურთ წევრის გაგდება და მომავალში გაწევრიანების აკრძალვა?" - "%1$s-ს გაგდება…" "მედიის შერჩევა ვერ მოხერხდა, გთხოვთ, სცადოთ ხელახლა." "მედიის ატვირთვა ვერ მოხერხდა. გთხოვთ, სცადოთ ხელახლა." "მედიის ატვირთვა ვერ მოხერხდა, გთხოვთ, სცადოთ ხელახლა." diff --git a/libraries/ui-strings/src/main/res/values-nb/translations.xml b/libraries/ui-strings/src/main/res/values-nb/translations.xml index ce8d655b6a..df9d892f14 100644 --- a/libraries/ui-strings/src/main/res/values-nb/translations.xml +++ b/libraries/ui-strings/src/main/res/values-nb/translations.xml @@ -339,18 +339,6 @@ Er du sikker på at du vil fortsette?" "Hei, snakk med meg på %1$s: %2$s" "%1$s Android" "Rageshake for å rapportere feil" - "Fjern og utesteng medlem" - "Utesteng" - "De vil ikke kunne bli med i dette rommet igjen hvis de blir invitert." - "Er du sikker på at du vil utestenge dette medlemmet?" - "Utestenger %1$s" - "Fjern" - "De vil kunne bli med i dette rommet igjen hvis de blir invitert." - "Er du sikker på at du vil fjerne dette medlemmet?" - "Vis profil" - "Fjern fra rommet" - "Fjerne medlem og utestenge fra å bli med i fremtiden?" - "Fjerner %1$s…" "Kunne ikke velge medium, prøv igjen." "Teksting er kanskje ikke synlig for personer som bruker eldre apper." "Kunne ikke behandle medier for opplasting, vennligst prøv igjen." diff --git a/libraries/ui-strings/src/main/res/values-nl/translations.xml b/libraries/ui-strings/src/main/res/values-nl/translations.xml index 0ad734232f..290720da86 100644 --- a/libraries/ui-strings/src/main/res/values-nl/translations.xml +++ b/libraries/ui-strings/src/main/res/values-nl/translations.xml @@ -292,15 +292,6 @@ Reden: %1$s." "Hé, praat met me op %1$s: %2$s" "%1$s Android" "Schudden om een bug te melden" - "Lid verwijderen en verbannen" - "Verbannen" - "Ze kunnen niet meer toetreden tot deze kamer als ze worden uitgenodigd." - "Weet je zeker dat je dit lid wilt verbannen?" - "%1$s verbannen" - "Profiel bekijken" - "Verwijderen uit kamer" - "Lid verwijderen en toekomstige deelname verbieden?" - "%1$s wordt verwijderd…" "Het selecteren van media is mislukt. Probeer het opnieuw." "Het verwerken van media voor uploaden is mislukt. Probeer het opnieuw." "Het uploaden van media is mislukt. Probeer het opnieuw." diff --git a/libraries/ui-strings/src/main/res/values-pl/translations.xml b/libraries/ui-strings/src/main/res/values-pl/translations.xml index e3e742072a..4d0941fcb1 100644 --- a/libraries/ui-strings/src/main/res/values-pl/translations.xml +++ b/libraries/ui-strings/src/main/res/values-pl/translations.xml @@ -343,18 +343,6 @@ Czy na pewno chcesz kontynuować?" "Hej, porozmawiajmy na %1$s: %2$s" "%1$s Android" "Wstrząśnij gniewnie, aby zgłosić błąd" - "Usuń i zbanuj członka" - "Zbanuj" - "Nie będą mogli ponownie dołączyć do tego pokoju, jeśli zostaną zaproszeni." - "Czy na pewno chcesz zbanować tego członka?" - "Banowanie %1$s" - "Usuń" - "Będą mogli ponownie dołączyć do pokoju, jeśli zostaną zaproszeni." - "Czy na pewno chcesz usunąć tego członka?" - "Wyświetl profil" - "Usuń z pokoju" - "Usunąć członka i zablokować możliwość dołączenia w przyszłości?" - "Usuwanie %1$s…" "Nie udało się wybrać multimediów. Spróbuj ponownie." "Opis może być niedostępny dla osób korzystających ze starszej wersji aplikacji." "Przetwarzanie multimediów do przesłania nie powiodło się, spróbuj ponownie." diff --git a/libraries/ui-strings/src/main/res/values-pt-rBR/translations.xml b/libraries/ui-strings/src/main/res/values-pt-rBR/translations.xml index b654dd355b..12df92cca6 100644 --- a/libraries/ui-strings/src/main/res/values-pt-rBR/translations.xml +++ b/libraries/ui-strings/src/main/res/values-pt-rBR/translations.xml @@ -287,15 +287,6 @@ "Ei, fale comigo em %1$s: %2$s" "%1$s Android" "Rageshake para relatar um bug" - "Remover e banir membro" - "Banir" - "Eles não poderão entrar nesta sala novamente se forem convidados." - "Tem certeza de que quer banir este membro?" - "Banindo %1$s" - "Ver perfil" - "Remover da sala" - "Remover membro e banir de entrar novamente no futuro?" - "Removendo %1$s…" "Falha ao selecionar a mídia, tente novamente." "Falha ao processar mídia para upload. Tente novamente." "Falha ao enviar mídia. Tente novamente." diff --git a/libraries/ui-strings/src/main/res/values-pt/translations.xml b/libraries/ui-strings/src/main/res/values-pt/translations.xml index 24fa3d7e35..696de1e319 100644 --- a/libraries/ui-strings/src/main/res/values-pt/translations.xml +++ b/libraries/ui-strings/src/main/res/values-pt/translations.xml @@ -324,16 +324,6 @@ Tens a certeza de que queres continuar?" "Alô! Fala comigo na %1$s: %2$s" "%1$s Android" "Agita o dispositivo em fúria para comunicar um problema" - "Remover e banir participante" - "Banir" - "Não poderão voltar a entrar nesta sala, mesmo se forem convidados." - "Tens a certeza que queres banir este participante?" - "A banir %1$s" - "Poderão entrar na sala novamente se convidados." - "Ver perfil" - "Remover da sala" - "Remover participante e proibir que entre no futuro?" - "A remover %1$s…" "Falha ao selecionar multimédia, por favor tente novamente." "As legendas poderão não ser visíveis em versões mais antigas da aplicação." "Falha ao processar multimédia para carregamento, por favor tente novamente." diff --git a/libraries/ui-strings/src/main/res/values-ro/translations.xml b/libraries/ui-strings/src/main/res/values-ro/translations.xml index 13a63c3ba9..98d849ce1b 100644 --- a/libraries/ui-strings/src/main/res/values-ro/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ro/translations.xml @@ -294,15 +294,6 @@ Motiv:%1$s." "Hei, vorbește cu mine pe %1$s: %2$s" "%1$s Android" "Rageshake pentru a raporta erori" - "Eliminați și interziceți membrul" - "Interzicere" - "Nu se vor putea alătura din nou acestei camere dacă sunt invitați." - "Sunteți sigur că doriți să interziceți acest membru?" - "Se interzice %1$s" - "Vizualizare profil" - "Înlăturați membrul" - "Înlăturați membrul și interziceți-i să se alăture în viitor?" - "Se elimină %1$s" "Selectarea fișierelor media a eșuat, încercați din nou." "Procesarea datelor media a eșuat, vă rugăm să încercați din nou." "Încărcarea fișierelor media a eșuat, încercați din nou." diff --git a/libraries/ui-strings/src/main/res/values-ru/translations.xml b/libraries/ui-strings/src/main/res/values-ru/translations.xml index 1b67806b09..7329755b34 100644 --- a/libraries/ui-strings/src/main/res/values-ru/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ru/translations.xml @@ -343,17 +343,6 @@ "Привет, поговори со мной по %1$s: %2$s" "%1$s Android" "Встряхните устройство, чтобы сообщить об ошибке" - "Удалить и заблокировать участника" - "Заблокировать" - "Они не смогут снова присоединиться к этой комнате, если их пригласят." - "Вы уверены, что хотите заблокировать этого участника?" - "Блокировка %1$s" - "Удалить" - "Вы действительно хотите удалить этого участника?" - "Посмотреть профиль" - "Удалить участника из комнаты" - "Удалить участника и запретить присоединяться в будущем?" - "Удаление %1$s…" "Не удалось выбрать носитель, попробуйте еще раз." "Подпись может быть не видна пользователям старых приложений." "Не удалось обработать медиафайл для загрузки, попробуйте еще раз." diff --git a/libraries/ui-strings/src/main/res/values-sk/translations.xml b/libraries/ui-strings/src/main/res/values-sk/translations.xml index 7c4302db9a..cffde3494a 100644 --- a/libraries/ui-strings/src/main/res/values-sk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml @@ -343,18 +343,6 @@ Naozaj chcete pokračovať?" "Ahoj, porozprávajte sa so mnou na %1$s: %2$s" "%1$s Android" "Zúrivo potriasť pre nahlásenie chyby" - "Odstrániť a zakázať člena" - "Zakázať" - "Nebudú sa môcť pripojiť k tejto miestnosti znova ani ak budú pozvaní." - "Ste si istý, že chcete zakázať tohto člena?" - "Zakazuje sa %1$s" - "Odstrániť" - "V prípade pozvania sa budú môcť znova pripojiť k tejto miestnosti." - "Ste si istý, že chcete odstrániť tohto člena?" - "Zobraziť profil" - "Odstrániť z miestnosti" - "Odstrániť člena a zakázať vstup v budúcnosti?" - "Odstraňuje sa %1$s…" "Nepodarilo sa vybrať médium, skúste to prosím znova." "Titulky nemusia byť viditeľné pre ľudí používajúcich staršie aplikácie." "Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova." diff --git a/libraries/ui-strings/src/main/res/values-sv/translations.xml b/libraries/ui-strings/src/main/res/values-sv/translations.xml index 0fda3671c3..098226a069 100644 --- a/libraries/ui-strings/src/main/res/values-sv/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sv/translations.xml @@ -339,18 +339,6 @@ Anledning:%1$s." "Hallå, prata med mig på %1$s: %2$s" "%1$s Android" "Raseriskaka för att rapportera bugg" - "Ta bort och banna medlem" - "Banna" - "Denne kommer inte att kunna gå med i det här rummet igen om denne bjuds in." - "Är du säker på att du vill banna den här medlemmen?" - "Bannar %1$s" - "Ta bort" - "Denne kommer kunna gå med i rummet igen om denne bjuds in" - "Är du säker på att du vill ta bort den här medlemmen?" - "Visa profil" - "Ta bort från rummet" - "Ta bort medlem och banna från att gå med i framtiden?" - "Tar bort %1$s …" "Misslyckades att välja media, vänligen pröva igen." "Bildtexter kanske inte är synliga för personer som använder äldre appar." "Misslyckades att bearbeta media för uppladdning, vänligen pröva igen." diff --git a/libraries/ui-strings/src/main/res/values-tr/translations.xml b/libraries/ui-strings/src/main/res/values-tr/translations.xml index 16766f12af..46f87e2019 100644 --- a/libraries/ui-strings/src/main/res/values-tr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-tr/translations.xml @@ -316,15 +316,6 @@ Neden: %1$s." "Hey, benimle konuş %1$s: %2$s" "%1$s Android" "Hata bildirmek için Rageshake" - "Üyeyi çıkar ve yasakla" - "Yasakla" - "Davet edilseler bile bu odaya tekrar katılamazlar." - "Bu üyeyi yasaklamak istediğinize emin misiniz?" - "Yasaklanıyor %1$s" - "Profili görüntüle" - "Odadan çıkar" - "Üyeyi çıkarın ve gelecekte katılmasını yasaklayın?" - "Kaldırılıyor %1$s…" "Medya seçilemedi, lütfen tekrar deneyin." "Açıklamalar, eski uygulamaları kullanan kişiler tarafından görülemeyebilir." "Medya yüklenemedi, lütfen tekrar deneyin." diff --git a/libraries/ui-strings/src/main/res/values-uk/translations.xml b/libraries/ui-strings/src/main/res/values-uk/translations.xml index f8a233d183..141a65850c 100644 --- a/libraries/ui-strings/src/main/res/values-uk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-uk/translations.xml @@ -343,18 +343,6 @@ "Вітаю, поспілкуйтеся зі мною в %1$s: %2$s" "%1$s Android" "Повідомити про ваду за допомогою Rageshake" - "Вилучити й заблокувати учасника" - "Заблокувати" - "Він не зможе приєднатися до цієї кімнати знову, якщо його запросять." - "Ви точно хочете заблокувати цього користувача?" - "Блокування %1$s" - "Вилучити" - "Вони зможуть знову приєднатися до цієї кімнати, якщо їх запросять." - "Ви дійсно хочете вилучити цього учасника?" - "Переглянути профіль" - "Вилучити з кімнати" - "Вилучити учасника та заборонити приєднання в майбутньому?" - "Вилучення %1$s…" "Не вдалося вибрати медіафайл, спробуйте ще раз." "Користувачі старих застосунків можуть не бачити підписи." "Не вдалося обробити медіафайл для завантаження, спробуйте ще раз." diff --git a/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml b/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml index ec0c7be7b6..8986bf6e79 100644 --- a/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml +++ b/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml @@ -333,18 +333,6 @@ "嘿,來 %1$s 和我聊天:%2$s" "%1$s Android" "憤怒搖晃以回報臭蟲" - "踢出並加入黑名單" - "加入黑名單" - "即使收到邀請,他們仍然無法加入聊天室。" - "您確定要將此成員加入黑名單?" - "正在將 %1$s 加入黑名單" - "移除" - "若收到邀請,他們可以再次加入此聊天室。" - "您真的想要移除此成員嗎?" - "查看個人檔案" - "踢出聊天室" - "移除成員並禁止未來再度加入?" - "正在踢出 %1$s…" "選取媒體失敗,請再試一次。" "使用舊應用程式的使用者可能看不到標題。" "無法處理要上傳的媒體,請再試一次。" diff --git a/libraries/ui-strings/src/main/res/values-zh/translations.xml b/libraries/ui-strings/src/main/res/values-zh/translations.xml index cd7cd61f9b..50eaf02824 100644 --- a/libraries/ui-strings/src/main/res/values-zh/translations.xml +++ b/libraries/ui-strings/src/main/res/values-zh/translations.xml @@ -327,18 +327,6 @@ "嗨!请通过 %1$s 与我联系:%2$s" "%1$s Android" "摇一摇以报错" - "移除并封禁成员" - "封禁" - "即使受到邀请,他们也无法再次加入聊天室。" - "您确定要封禁该成员吗?" - "封禁 %1$s" - "移除" - "如果受到邀请,他们可以重新加入聊天室。" - "您确定要移除此成员吗?" - "查看个人资料" - "从聊天室移除" - "删除成员并禁止重新加入?" - "正在移除 %1$s……" "选择媒体失败,请重试。" "使用旧版应用程序的用户可能无法看到字幕。" "处理要上传的媒体失败,请重试。" diff --git a/tools/localazy/config.json b/tools/localazy/config.json index e5d35def0c..51db790fd8 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -325,6 +325,14 @@ "includeRegex" : [ "screen\\.report_room\\..*" ] + }, + { + "name" : ":features:roommembermoderation:impl", + "includeRegex" : [ + "screen\\.bottom_sheet\\.manage_room_member\\..*", + "screen_room_member_list_manage_member.*", + "screen_room_member_list_unbanning_user" + ] } ] } From a09cc8de979f2dadf96ddb0aa4808cff04316bb1 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 13 May 2025 11:39:19 +0200 Subject: [PATCH 02/50] change (member moderation) : branch moderation on timeline --- features/messages/impl/build.gradle.kts | 1 + .../features/messages/impl/MessagesEvents.kt | 3 + .../features/messages/impl/MessagesNode.kt | 16 ++++- .../messages/impl/MessagesPresenter.kt | 9 ++- .../features/messages/impl/MessagesState.kt | 2 + .../messages/impl/MessagesStateProvider.kt | 15 +++++ .../features/messages/impl/MessagesView.kt | 7 +- .../pinned/list/PinnedMessagesListNode.kt | 5 +- .../pinned/list/PinnedMessagesListView.kt | 7 +- .../messages/impl/timeline/TimelineView.kt | 3 +- .../components/TimelineItemEventRow.kt | 13 +++- .../TimelineItemGroupedEventsRow.kt | 5 +- .../timeline/components/TimelineItemRow.kt | 3 +- .../impl/members/RoomMemberListNode.kt | 2 +- .../impl/members/RoomMemberListPresenter.kt | 5 +- .../api/RoomMemberModerationEvents.kt | 6 +- .../api/RoomMemberModerationState.kt | 10 +-- .../impl/InternalRoomMemberModerationState.kt | 7 +- .../impl/RoomMemberModerationPresenter.kt | 67 +++++++++++-------- .../impl/RoomMemberModerationStateProvider.kt | 29 ++++---- .../impl/RoomMemberModerationView.kt | 42 ++++++------ .../item/event/ProfileTimelineDetails.kt | 7 ++ 22 files changed, 169 insertions(+), 95 deletions(-) diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index cf8f73f47a..5789afab57 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -67,6 +67,7 @@ dependencies { implementation(libs.telephoto.zoomableimage) implementation(libs.matrix.emojibase.bindings) implementation(projects.features.knockrequests.api) + implementation(projects.features.roommembermoderation.api) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesEvents.kt index 8d4d597502..f299deaf0e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesEvents.kt @@ -9,12 +9,15 @@ package io.element.android.features.messages.impl import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId +import io.element.android.libraries.matrix.api.user.MatrixUser sealed interface MessagesEvents { data class HandleAction(val action: TimelineItemAction, val event: TimelineItem.Event) : MessagesEvents data class ToggleReaction(val emoji: String, val eventOrTransactionId: EventOrTransactionId) : MessagesEvents data class InviteDialogDismissed(val action: InviteDialogAction) : MessagesEvents + data class OnUserClicked(val user: MatrixUser) : MessagesEvents data object Dismiss : MessagesEvents } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt index d2d4b1c705..5f2546dd50 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt @@ -39,6 +39,9 @@ import io.element.android.features.messages.impl.timeline.TimelinePresenter import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.roommembermoderation.api.ModerationAction +import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationRenderer import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab import io.element.android.libraries.androidutils.system.openUrlInExternalApp import io.element.android.libraries.androidutils.system.toast @@ -76,7 +79,8 @@ class MessagesNode @AssistedInject constructor( private val timelineItemPresenterFactories: TimelineItemPresenterFactories, private val mediaPlayer: MediaPlayer, private val permalinkParser: PermalinkParser, - private val knockRequestsBannerRenderer: KnockRequestsBannerRenderer + private val knockRequestsBannerRenderer: KnockRequestsBannerRenderer, + private val roomMemberModerationRenderer: RoomMemberModerationRenderer, ) : Node(buildContext, plugins = plugins), MessagesNavigator { private val presenter = presenterFactory.create( navigator = this, @@ -257,6 +261,16 @@ class MessagesNode @AssistedInject constructor( }, modifier = modifier, ) + roomMemberModerationRenderer.Render( + state = state.roomMemberModerationState, + onSelectAction = { action -> + when (action) { + is ModerationAction.DisplayProfile -> onUserDataClick(action.user.userId) + else -> state.roomMemberModerationState.eventSink(RoomMemberModerationEvents.ProcessAction(action)) + } + }, + modifier = Modifier, + ) var focusedEventId by rememberSaveable { mutableStateOf(inputs.focusedEventId) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index 8bf5d330d7..6be5646649 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -50,6 +50,8 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState import io.element.android.features.roomcall.api.RoomCallState +import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.androidutils.clipboard.ClipboardHelper import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter @@ -103,6 +105,7 @@ class MessagesPresenter @AssistedInject constructor( private val readReceiptBottomSheetPresenter: Presenter, private val pinnedMessagesBannerPresenter: Presenter, private val roomCallStatePresenter: Presenter, + private val roomMemberModerationPresenter: Presenter, private val syncService: SyncService, private val snackbarDispatcher: SnackbarDispatcher, private val dispatchers: CoroutineDispatchers, @@ -143,7 +146,7 @@ class MessagesPresenter @AssistedInject constructor( val readReceiptBottomSheetState = readReceiptBottomSheetPresenter.present() val pinnedMessagesBannerState = pinnedMessagesBannerPresenter.present() val roomCallState = roomCallStatePresenter.present() - + val roomMemberModerationState = roomMemberModerationPresenter.present() val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val userEventPermissions by userEventPermissions(syncUpdateFlow.value) @@ -233,6 +236,9 @@ class MessagesPresenter @AssistedInject constructor( } } is MessagesEvents.Dismiss -> actionListState.eventSink(ActionListEvents.Clear) + is MessagesEvents.OnUserClicked -> { + roomMemberModerationState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(event.user)) + } } } @@ -262,6 +268,7 @@ class MessagesPresenter @AssistedInject constructor( roomCallState = roomCallState, pinnedMessagesBannerState = pinnedMessagesBannerState, dmUserVerificationState = dmUserVerificationState, + roomMemberModerationState = roomMemberModerationState, eventSink = { handleEvents(it) } ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt index 2a6889be41..8ba2cbd039 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt @@ -20,6 +20,7 @@ import io.element.android.features.messages.impl.timeline.components.receipt.bot import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState import io.element.android.features.roomcall.api.RoomCallState +import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage @@ -54,5 +55,6 @@ data class MessagesState( val appName: String, val pinnedMessagesBannerState: PinnedMessagesBannerState, val dmUserVerificationState: IdentityState?, + val roomMemberModerationState: RoomMemberModerationState, val eventSink: (MessagesEvents) -> Unit ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index 12c6d607b0..debee82b3a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -37,11 +37,14 @@ import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMe import io.element.android.features.roomcall.api.RoomCallState import io.element.android.features.roomcall.api.aStandByCallState import io.element.android.features.roomcall.api.anOngoingCallState +import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.encryption.identity.IdentityState +import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.textcomposer.model.aTextEditorStateRich import kotlinx.collections.immutable.persistentListOf @@ -116,6 +119,7 @@ fun aMessagesState( roomCallState: RoomCallState = aStandByCallState(), pinnedMessagesBannerState: PinnedMessagesBannerState = aLoadedPinnedMessagesBannerState(), dmUserVerificationState: IdentityState? = null, + roomMemberModerationState: RoomMemberModerationState = aRoomMemberModerationState(), eventSink: (MessagesEvents) -> Unit = {}, ) = MessagesState( roomId = RoomId("!id:domain"), @@ -143,9 +147,20 @@ fun aMessagesState( appName = "Element", pinnedMessagesBannerState = pinnedMessagesBannerState, dmUserVerificationState = dmUserVerificationState, + roomMemberModerationState = roomMemberModerationState, eventSink = eventSink, ) +fun aRoomMemberModerationState( + canKick: Boolean = false, + canBan: Boolean = false, + +) = object : RoomMemberModerationState { + override val canKick: Boolean = canKick + override val canBan: Boolean = canBan + override val eventSink: (RoomMemberModerationEvents) -> Unit = {} +} + fun aUserEventPermissions( canRedactOwn: Boolean = false, canRedactOther: Boolean = false, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 948b8c3cb3..262698894a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -103,6 +103,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbar import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.encryption.identity.IdentityState +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.textcomposer.model.TextEditorState import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.wysiwyg.link.Link @@ -208,7 +209,9 @@ fun MessagesView( .consumeWindowInsets(padding), onContentClick = ::onContentClick, onMessageLongClick = ::onMessageLongClick, - onUserDataClick = { hidingKeyboard { onUserDataClick(it) } }, + onUserDataClick = { hidingKeyboard { + state.eventSink(MessagesEvents.OnUserClicked(it)) + } }, onLinkClick = { link, customTab -> if (customTab) { onLinkClick(link.url, true) @@ -293,7 +296,7 @@ private fun ReinviteDialog(state: MessagesState) { private fun MessagesViewContent( state: MessagesState, onContentClick: (TimelineItem.Event) -> Unit, - onUserDataClick: (UserId) -> Unit, + onUserDataClick: (MatrixUser) -> Unit, onLinkClick: (Link, Boolean) -> Unit, onReactionClick: (key: String, TimelineItem.Event) -> Unit, onReactionLongClick: (key: String, TimelineItem.Event) -> Unit, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt index 9827698f99..2dbf76bc8d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt @@ -33,6 +33,7 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.ui.strings.CommonStrings @ContributesNode(RoomScope::class) @@ -63,8 +64,8 @@ class PinnedMessagesListNode @AssistedInject constructor( return callbacks.forEach { it.onEventClick(event) } } - private fun onUserDataClick(userId: UserId) { - callbacks.forEach { it.onUserDataClick(userId) } + private fun onUserDataClick(user: MatrixUser) { + callbacks.forEach { it.onUserDataClick(user.userId) } } private fun onLinkClick(context: Context, url: String) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt index c87e4c0ffd..e0356f1670 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt @@ -49,6 +49,7 @@ import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.services.analytics.compose.LocalAnalyticsService import io.element.android.services.analyticsproviders.api.trackers.captureInteraction @@ -59,7 +60,7 @@ fun PinnedMessagesListView( state: PinnedMessagesListState, onBackClick: () -> Unit, onEventClick: (event: TimelineItem.Event) -> Unit, - onUserDataClick: (UserId) -> Unit, + onUserDataClick: (MatrixUser) -> Unit, onLinkClick: (Link) -> Unit, onLinkLongClick: (Link) -> Unit, modifier: Modifier = Modifier, @@ -115,7 +116,7 @@ private fun PinnedMessagesListTopBar( private fun PinnedMessagesListContent( state: PinnedMessagesListState, onEventClick: (event: TimelineItem.Event) -> Unit, - onUserDataClick: (UserId) -> Unit, + onUserDataClick: (MatrixUser) -> Unit, onLinkClick: (Link) -> Unit, onLinkLongClick: (Link) -> Unit, onErrorDismiss: () -> Unit, @@ -171,7 +172,7 @@ private fun PinnedMessagesListEmpty( private fun PinnedMessagesListLoaded( state: PinnedMessagesListState.Filled, onEventClick: (event: TimelineItem.Event) -> Unit, - onUserDataClick: (UserId) -> Unit, + onUserDataClick: (MatrixUser) -> Unit, onLinkClick: (Link) -> Unit, onLinkLongClick: (Link) -> Unit, modifier: Modifier = Modifier, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index c64eec9bee..b14ff51ce2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -71,6 +71,7 @@ import io.element.android.libraries.designsystem.utils.animateScrollToItemCenter import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.CommonStrings @@ -92,7 +93,7 @@ import kotlin.time.Duration.Companion.milliseconds fun TimelineView( state: TimelineState, timelineProtectionState: TimelineProtectionState, - onUserDataClick: (UserId) -> Unit, + onUserDataClick: (MatrixUser) -> Unit, onLinkClick: (Link) -> Unit, onContentClick: (TimelineItem.Event) -> Unit, onMessageLongClick: (TimelineItem.Event) -> Unit, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt index f3b890cb18..e38d212041 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt @@ -87,6 +87,10 @@ import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails +import io.element.android.libraries.matrix.api.timeline.item.event.getAvatarUrl +import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName +import io.element.android.libraries.matrix.api.timeline.item.event.getDisplayName +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails import io.element.android.libraries.matrix.ui.messages.reply.InReplyToView import io.element.android.libraries.matrix.ui.messages.reply.eventId @@ -122,7 +126,7 @@ fun TimelineItemEventRow( onLongClick: () -> Unit, onLinkClick: (Link) -> Unit, onLinkLongClick: (Link) -> Unit, - onUserDataClick: (UserId) -> Unit, + onUserDataClick: (MatrixUser) -> Unit, inReplyToClick: (EventId) -> Unit, onReactionClick: (emoji: String, eventId: TimelineItem.Event) -> Unit, onReactionLongClick: (emoji: String, eventId: TimelineItem.Event) -> Unit, @@ -160,7 +164,12 @@ fun TimelineItemEventRow( } fun onUserDataClick() { - onUserDataClick(event.senderId) + val sender = MatrixUser( + userId = event.senderId, + displayName = event.senderProfile.getDisplayName(), + avatarUrl = event.senderProfile.getAvatarUrl(), + ) + onUserDataClick(sender) } fun inReplyToClick() { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt index 8f868370b3..4e55d6c3ff 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt @@ -35,6 +35,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.wysiwyg.link.Link @Composable @@ -48,7 +49,7 @@ fun TimelineItemGroupedEventsRow( onClick: (TimelineItem.Event) -> Unit, onLongClick: (TimelineItem.Event) -> Unit, inReplyToClick: (EventId) -> Unit, - onUserDataClick: (UserId) -> Unit, + onUserDataClick: (MatrixUser) -> Unit, onLinkClick: (Link) -> Unit, onLinkLongClick: (Link) -> Unit, onReactionClick: (key: String, TimelineItem.Event) -> Unit, @@ -117,7 +118,7 @@ private fun TimelineItemGroupedEventsRowContent( onClick: (TimelineItem.Event) -> Unit, onLongClick: (TimelineItem.Event) -> Unit, inReplyToClick: (EventId) -> Unit, - onUserDataClick: (UserId) -> Unit, + onUserDataClick: (MatrixUser) -> Unit, onLinkClick: (Link) -> Unit, onLinkLongClick: (Link) -> Unit, onReactionClick: (key: String, TimelineItem.Event) -> Unit, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt index 88b19c43fe..136cdcbfed 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt @@ -43,6 +43,7 @@ import io.element.android.libraries.designsystem.theme.LocalBuildMeta import io.element.android.libraries.designsystem.theme.highlightedMessageBackgroundColor import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.utils.time.isTalkbackActive import io.element.android.wysiwyg.link.Link @@ -57,7 +58,7 @@ internal fun TimelineItemRow( isLastOutgoingMessage: Boolean, timelineProtectionState: TimelineProtectionState, focusedEventId: EventId?, - onUserDataClick: (UserId) -> Unit, + onUserDataClick: (MatrixUser) -> Unit, onLinkClick: (Link) -> Unit, onLinkLongClick: (Link) -> Unit, onContentClick: (TimelineItem.Event) -> Unit, 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 05a35fad88..fd9dc1cd51 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 @@ -77,7 +77,7 @@ class RoomMemberListNode @AssistedInject constructor( state = state.moderationState, onSelectAction = { action -> when (action) { - is ModerationAction.DisplayProfile -> openRoomMemberDetails(action.member.userId) + is ModerationAction.DisplayProfile -> openRoomMemberDetails(action.user.userId) else -> state.moderationState.eventSink(RoomMemberModerationEvents.ProcessAction(action)) } }, 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 07688387e3..0066b80231 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 @@ -34,6 +34,7 @@ import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMembersState import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.room.roomMembers +import io.element.android.libraries.matrix.api.room.toMatrixUser import io.element.android.libraries.matrix.ui.room.canInviteAsState import io.element.android.libraries.matrix.ui.room.isDmAsState import io.element.android.libraries.matrix.ui.room.roomMemberIdentityStateChange @@ -166,9 +167,9 @@ class RoomMemberListPresenter @AssistedInject constructor( is RoomMemberListEvents.UpdateSearchQuery -> searchQuery = event.query is RoomMemberListEvents.RoomMemberSelected -> if (event.roomMember.membership == RoomMembershipState.BAN) { - roomModerationState.eventSink(RoomMemberModerationEvents.ProcessAction(ModerationAction.UnbanUser(event.roomMember))) + roomModerationState.eventSink(RoomMemberModerationEvents.ProcessAction(ModerationAction.UnbanUser(event.roomMember.toMatrixUser()))) } else if (!isDm.value && (roomModerationState.canBan || roomModerationState.canKick)) { - roomModerationState.eventSink(RoomMemberModerationEvents.RenderActions(event.roomMember)) + roomModerationState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(event.roomMember.toMatrixUser())) } else { navigator.openRoomMemberDetails(event.roomMember.userId) } diff --git a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationEvents.kt b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationEvents.kt index 265a59a125..277b4275d6 100644 --- a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationEvents.kt +++ b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationEvents.kt @@ -7,9 +7,9 @@ package io.element.android.features.roommembermoderation.api -import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.user.MatrixUser interface RoomMemberModerationEvents { - data class RenderActions(val roomMember: RoomMember) : RoomMemberModerationEvents - data class ProcessAction(val action: ModerationAction): RoomMemberModerationEvents + data class ShowActionsForUser(val user: MatrixUser) : RoomMemberModerationEvents + data class ProcessAction(val action: ModerationAction) : RoomMemberModerationEvents } diff --git a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationState.kt b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationState.kt index 410b8d6423..b31857b594 100644 --- a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationState.kt +++ b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationState.kt @@ -7,7 +7,7 @@ package io.element.android.features.roommembermoderation.api -import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.user.MatrixUser interface RoomMemberModerationState { val canKick: Boolean @@ -16,8 +16,8 @@ interface RoomMemberModerationState { } sealed interface ModerationAction { - data class DisplayProfile(val member: RoomMember) : ModerationAction - data class KickUser(val member: RoomMember) : ModerationAction - data class BanUser(val member: RoomMember) : ModerationAction - data class UnbanUser(val member: RoomMember) : ModerationAction + data class DisplayProfile(val user: MatrixUser) : ModerationAction + data class KickUser(val user: MatrixUser) : ModerationAction + data class BanUser(val user: MatrixUser) : ModerationAction + data class UnbanUser(val user: MatrixUser) : ModerationAction } diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationState.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationState.kt index e6258f2003..7e0c27a62d 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationState.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationState.kt @@ -13,20 +13,19 @@ import io.element.android.features.roommembermoderation.api.RoomMemberModeration import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.user.MatrixUser import kotlinx.collections.immutable.ImmutableList data class InternalRoomMemberModerationState( override val canKick: Boolean, override val canBan: Boolean, - val selectedRoomMember: AsyncData, + val selectedUser: MatrixUser?, val actions: ImmutableList, val kickUserAsyncAction: AsyncAction, val banUserAsyncAction: AsyncAction, val unbanUserAsyncAction: AsyncAction, override val eventSink: (RoomMemberModerationEvents) -> Unit, ) : RoomMemberModerationState { - - val canOnlyDisplayProfile = actions.size == 1 && actions.first() is ModerationAction.DisplayProfile - val canDisplayActions = actions.isNotEmpty() && !canOnlyDisplayProfile + val canDisplayActions = actions.isNotEmpty() } diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt index 5f7fa80c60..fb9199ea54 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt @@ -20,13 +20,14 @@ import io.element.android.features.roommembermoderation.api.ModerationAction import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runUpdatingState import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.toMatrixUser +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.room.canBanAsState import io.element.android.libraries.matrix.ui.room.canKickAsState import io.element.android.libraries.matrix.ui.room.userPowerLevelAsState @@ -45,7 +46,6 @@ class RoomMemberModerationPresenter @Inject constructor( private val dispatchers: CoroutineDispatchers, private val analyticsService: AnalyticsService, ) : Presenter { - private var selectedMember by mutableStateOf>(AsyncData.Uninitialized) @Composable override fun present(): RoomMemberModerationState { @@ -61,60 +61,68 @@ class RoomMemberModerationPresenter @Inject constructor( remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction) } val unbanUserAsyncAction = remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction) } - + var selectedUser by remember { + mutableStateOf(null) + } val moderationActions = remember { mutableStateOf(persistentListOf()) } fun handleEvent(event: RoomMemberModerationEvents) { when (event) { - is RoomMemberModerationEvents.RenderActions -> { - selectedMember = AsyncData.Success(event.roomMember) - moderationActions.value = computeModerationActions( - member = event.roomMember, - canKick = canKick.value, - canBan = canBan.value, - currentUserMemberPowerLevel = currentUserMemberPowerLevel.value, - ) + is RoomMemberModerationEvents.ShowActionsForUser -> { + coroutineScope.launch { + selectedUser = event.user + moderationActions.value = persistentListOf(ModerationAction.DisplayProfile(event.user)) + room.getUpdatedMember(event.user.userId) + .onSuccess { + moderationActions.value = computeModerationActions( + member = it, + canKick = canKick.value, + canBan = canBan.value, + currentUserMemberPowerLevel = currentUserMemberPowerLevel.value, + ) + } + } } is RoomMemberModerationEvents.ProcessAction -> { - when(val action = event.action) { + when (val action = event.action) { is ModerationAction.DisplayProfile -> Unit is ModerationAction.KickUser -> { - selectedMember = AsyncData.Success(action.member) + selectedUser = action.user kickUserAsyncAction.value = AsyncAction.ConfirmingNoParams } is ModerationAction.BanUser -> { - selectedMember = AsyncData.Success(action.member) + selectedUser = action.user banUserAsyncAction.value = AsyncAction.ConfirmingNoParams } is ModerationAction.UnbanUser -> { - selectedMember = AsyncData.Success(action.member) + selectedUser = action.user unbanUserAsyncAction.value = AsyncAction.ConfirmingNoParams } } } is InternalRoomMemberModerationEvents.DoKickUser -> { - selectedMember.dataOrNull()?.let { + selectedUser?.let { coroutineScope.kickUser(it.userId, event.reason, kickUserAsyncAction) } - selectedMember = AsyncData.Uninitialized + selectedUser = null } is InternalRoomMemberModerationEvents.DoBanUser -> { - selectedMember.dataOrNull()?.let { + selectedUser?.let { coroutineScope.banUser(it.userId, event.reason, banUserAsyncAction) } - selectedMember = AsyncData.Uninitialized + selectedUser = null } is InternalRoomMemberModerationEvents.Reset -> { - selectedMember = AsyncData.Uninitialized + selectedUser = null kickUserAsyncAction.value = AsyncAction.Uninitialized banUserAsyncAction.value = AsyncAction.Uninitialized unbanUserAsyncAction.value = AsyncAction.Uninitialized } is InternalRoomMemberModerationEvents.DoUnbanUser -> { - selectedMember.dataOrNull()?.let { + selectedUser?.let { coroutineScope.unbanUser(it.userId, unbanUserAsyncAction) } - selectedMember = AsyncData.Uninitialized + selectedUser = null } } } @@ -122,7 +130,7 @@ class RoomMemberModerationPresenter @Inject constructor( return InternalRoomMemberModerationState( canKick = canKick.value, canBan = canBan.value, - selectedRoomMember = selectedMember, + selectedUser = selectedUser, actions = moderationActions.value, kickUserAsyncAction = kickUserAsyncAction.value, banUserAsyncAction = banUserAsyncAction.value, @@ -137,13 +145,15 @@ class RoomMemberModerationPresenter @Inject constructor( canBan: Boolean, currentUserMemberPowerLevel: Long, ): PersistentList { + val memberAsUser = member.toMatrixUser() return buildList { - add(ModerationAction.DisplayProfile(member)) - if (canKick && member.powerLevel < currentUserMemberPowerLevel) { - add(ModerationAction.KickUser(member)) + add(ModerationAction.DisplayProfile(memberAsUser)) + val canModerateThisUser = member.powerLevel < currentUserMemberPowerLevel && member.membership.isActive() + if (canKick && canModerateThisUser) { + add(ModerationAction.KickUser(memberAsUser)) } - if (canBan && member.powerLevel < currentUserMemberPowerLevel) { - add(ModerationAction.BanUser(member)) + if (canBan && canModerateThisUser) { + add(ModerationAction.BanUser(memberAsUser)) } }.toPersistentList() } @@ -194,4 +204,5 @@ class RoomMemberModerationPresenter @Inject constructor( } } } + } diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationStateProvider.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationStateProvider.kt index b705e5217d..9d52eec503 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationStateProvider.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationStateProvider.kt @@ -15,26 +15,28 @@ import io.element.android.libraries.architecture.AsyncData 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.RoomMembershipState +import io.element.android.libraries.matrix.api.room.toMatrixUser +import io.element.android.libraries.matrix.api.user.MatrixUser import kotlinx.collections.immutable.toPersistentList class RoomMemberModerationStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aRoomMembersModerationState( - selectedRoomMember = AsyncData.Success(anAlice()), + selectedUser = anAlice(), actions = listOf( ModerationAction.DisplayProfile(anAlice()), ), ), aRoomMembersModerationState( - selectedRoomMember = AsyncData.Success(anAlice()), + selectedUser = anAlice(), actions = listOf( ModerationAction.DisplayProfile(anAlice()), ModerationAction.KickUser(anAlice()), ), ), aRoomMembersModerationState( - selectedRoomMember = AsyncData.Success(anAlice()), + selectedUser = anAlice(), actions = listOf( ModerationAction.DisplayProfile(anAlice()), ModerationAction.KickUser(anAlice()), @@ -42,41 +44,34 @@ class RoomMemberModerationStateProvider : PreviewParameterProvider = AsyncData.Uninitialized, + selectedUser: MatrixUser? = null, actions: List = emptyList(), kickUserAsyncAction: AsyncAction = AsyncAction.Uninitialized, banUserAsyncAction: AsyncAction = AsyncAction.Uninitialized, @@ -85,7 +80,7 @@ fun aRoomMembersModerationState( ) = InternalRoomMemberModerationState( canKick = canKick, canBan = canBan, - selectedRoomMember = selectedRoomMember, + selectedUser = selectedUser, actions = actions.toPersistentList(), kickUserAsyncAction = kickUserAsyncAction, banUserAsyncAction = banUserAsyncAction, diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt index ff949fde59..31b474057d 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt @@ -20,9 +20,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -49,9 +47,9 @@ 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.room.RoomMember -import io.element.android.libraries.matrix.api.room.getBestName +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.model.getAvatarData +import io.element.android.libraries.matrix.ui.model.getBestName import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.launch @@ -63,23 +61,27 @@ fun RoomMemberModerationView( onSelectAction: (ModerationAction) -> Unit, modifier: Modifier = Modifier, ) { - val selectedRoomMember = state.selectedRoomMember.dataOrNull() Box(modifier = modifier) { - if (selectedRoomMember != null && state.canDisplayActions) { + val selectedUser = state.selectedUser + if (selectedUser != null && state.canDisplayActions) { RoomMemberActionsBottomSheet( - roomMember = selectedRoomMember, + user = selectedUser, actions = state.actions, onSelectAction = onSelectAction, onDismiss = { state.eventSink(InternalRoomMemberModerationEvents.Reset) }, ) } - val onSelectAction by rememberUpdatedState(onSelectAction) - LaunchedEffect(state.canOnlyDisplayProfile) { - if (state.canOnlyDisplayProfile) { - onSelectAction(state.actions.first()) - } - } + RoomMemberAsyncActions(state = state) + } +} +@Composable +private fun RoomMemberAsyncActions( + state: InternalRoomMemberModerationState, + modifier: Modifier = Modifier, +) { + Box(modifier = modifier) { + val selectedUser = state.selectedUser val asyncIndicatorState = rememberAsyncIndicatorState() AsyncIndicatorHost(modifier = Modifier.statusBarsPadding(), state = asyncIndicatorState) @@ -100,7 +102,7 @@ fun RoomMemberModerationView( } is AsyncAction.Loading -> { LaunchedEffect(action) { - val userDisplayName = selectedRoomMember?.getBestName().orEmpty() + val userDisplayName = selectedUser?.getBestName().orEmpty() asyncIndicatorState.enqueue { AsyncIndicator.Loading(text = stringResource(R.string.screen_bottom_sheet_manage_room_member_removing_user, userDisplayName)) } @@ -139,7 +141,7 @@ fun RoomMemberModerationView( } is AsyncAction.Loading -> { LaunchedEffect(action) { - val userDisplayName = selectedRoomMember?.getBestName().orEmpty() + val userDisplayName = selectedUser?.getBestName().orEmpty() asyncIndicatorState.enqueue { AsyncIndicator.Loading(text = stringResource(R.string.screen_bottom_sheet_manage_room_member_banning_user, userDisplayName)) } @@ -167,7 +169,7 @@ fun RoomMemberModerationView( content = stringResource(R.string.screen_room_member_list_manage_member_unban_message), submitText = stringResource(R.string.screen_room_member_list_manage_member_unban_action), onSubmitClick = { - val userDisplayName = selectedRoomMember?.getBestName().orEmpty() + val userDisplayName = selectedUser?.getBestName().orEmpty() asyncIndicatorState.enqueue { AsyncIndicator.Loading(text = stringResource(R.string.screen_room_member_list_unbanning_user, userDisplayName)) } @@ -198,7 +200,7 @@ fun RoomMemberModerationView( @OptIn(ExperimentalMaterial3Api::class) @Composable private fun RoomMemberActionsBottomSheet( - roomMember: RoomMember, + user: MatrixUser, actions: ImmutableList, onSelectAction: (ModerationAction) -> Unit, onDismiss: () -> Unit, @@ -219,12 +221,12 @@ private fun RoomMemberActionsBottomSheet( modifier = Modifier.padding(vertical = 16.dp) ) { Avatar( - avatarData = roomMember.getAvatarData(size = AvatarSize.RoomListManageUser), + avatarData = user.getAvatarData(size = AvatarSize.RoomListManageUser), modifier = Modifier .padding(bottom = 28.dp) .align(Alignment.CenterHorizontally) ) - roomMember.displayName?.let { + user.displayName?.let { Text( text = it, style = ElementTheme.typography.fontHeadingLgBold, @@ -237,7 +239,7 @@ private fun RoomMemberActionsBottomSheet( ) } Text( - text = roomMember.userId.toString(), + text = user.userId.toString(), style = ElementTheme.typography.fontBodyLgRegular, color = ElementTheme.colors.textSecondary, maxLines = 1, diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/ProfileTimelineDetails.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/ProfileTimelineDetails.kt index ff1f9351f9..46ab482ad5 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/ProfileTimelineDetails.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/ProfileTimelineDetails.kt @@ -44,6 +44,13 @@ fun ProfileTimelineDetails.getDisambiguatedDisplayName(userId: UserId): String { } } +fun ProfileTimelineDetails.getDisplayName(): String? { + return when (this) { + is ProfileTimelineDetails.Ready -> displayName + else -> null + } +} + fun ProfileTimelineDetails.getAvatarUrl(): String? { return when (this) { is ProfileTimelineDetails.Ready -> avatarUrl From 6021fc9c3f9c590a27a988f8857e29fe6222b4a1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 16 May 2025 15:51:22 +0000 Subject: [PATCH 03/50] fix(deps): update media3 to v1.7.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0da9df9aa9..1eb476fed4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,7 +16,7 @@ constraintlayout = "2.2.1" constraintlayout_compose = "1.1.1" lifecycle = "2.9.0" activity = "1.10.1" -media3 = "1.6.1" +media3 = "1.7.1" camera = "1.4.2" # Compose From e405bf80a67c979a0dc89029ada1af2d0e9d7c75 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 16 May 2025 18:38:15 +0200 Subject: [PATCH 04/50] change (member moderation) : allow disabled action and render unban too --- .../features/messages/impl/MessagesNode.kt | 6 +- .../impl/members/RoomMemberListNode.kt | 6 +- .../impl/members/RoomMemberListPresenter.kt | 2 +- .../api/RoomMemberModerationEvents.kt | 2 +- .../api/RoomMemberModerationRenderer.kt | 3 +- .../api/RoomMemberModerationState.kt | 15 ++-- .../DefaultRoomMemberModerationRenderer.kt | 3 +- .../impl/InternalRoomMemberModerationState.kt | 3 +- .../impl/RoomMemberModerationPresenter.kt | 69 ++++++++++--------- .../impl/RoomMemberModerationStateProvider.kt | 23 +++++-- .../impl/RoomMemberModerationView.kt | 64 +++++++++++------ 11 files changed, 119 insertions(+), 77 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt index 5f2546dd50..6acf48e58b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt @@ -263,10 +263,10 @@ class MessagesNode @AssistedInject constructor( ) roomMemberModerationRenderer.Render( state = state.roomMemberModerationState, - onSelectAction = { action -> + onSelectAction = { action, target -> when (action) { - is ModerationAction.DisplayProfile -> onUserDataClick(action.user.userId) - else -> state.roomMemberModerationState.eventSink(RoomMemberModerationEvents.ProcessAction(action)) + is ModerationAction.DisplayProfile -> onUserDataClick(target.userId) + else -> state.roomMemberModerationState.eventSink(RoomMemberModerationEvents.ProcessAction(action, target)) } }, modifier = Modifier, 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 fd9dc1cd51..cf2f89c41e 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 @@ -75,10 +75,10 @@ class RoomMemberListNode @AssistedInject constructor( ) roomMemberModerationRenderer.Render( state = state.moderationState, - onSelectAction = { action -> + onSelectAction = { action, target -> when (action) { - is ModerationAction.DisplayProfile -> openRoomMemberDetails(action.user.userId) - else -> state.moderationState.eventSink(RoomMemberModerationEvents.ProcessAction(action)) + is ModerationAction.DisplayProfile -> openRoomMemberDetails(target.userId) + else -> state.moderationState.eventSink(RoomMemberModerationEvents.ProcessAction(action, target)) } }, modifier = Modifier, 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 0066b80231..c637f338cb 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 @@ -167,7 +167,7 @@ class RoomMemberListPresenter @AssistedInject constructor( is RoomMemberListEvents.UpdateSearchQuery -> searchQuery = event.query is RoomMemberListEvents.RoomMemberSelected -> if (event.roomMember.membership == RoomMembershipState.BAN) { - roomModerationState.eventSink(RoomMemberModerationEvents.ProcessAction(ModerationAction.UnbanUser(event.roomMember.toMatrixUser()))) + roomModerationState.eventSink(RoomMemberModerationEvents.ProcessAction(ModerationAction.UnbanUser, event.roomMember.toMatrixUser())) } else if (!isDm.value && (roomModerationState.canBan || roomModerationState.canKick)) { roomModerationState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(event.roomMember.toMatrixUser())) } else { diff --git a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationEvents.kt b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationEvents.kt index 277b4275d6..94c7477739 100644 --- a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationEvents.kt +++ b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationEvents.kt @@ -11,5 +11,5 @@ import io.element.android.libraries.matrix.api.user.MatrixUser interface RoomMemberModerationEvents { data class ShowActionsForUser(val user: MatrixUser) : RoomMemberModerationEvents - data class ProcessAction(val action: ModerationAction) : RoomMemberModerationEvents + data class ProcessAction(val action: ModerationAction, val targetUser: MatrixUser) : RoomMemberModerationEvents } diff --git a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationRenderer.kt b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationRenderer.kt index 2911bf1b8b..afe8ab0f8b 100644 --- a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationRenderer.kt +++ b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationRenderer.kt @@ -9,12 +9,13 @@ package io.element.android.features.roommembermoderation.api import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import io.element.android.libraries.matrix.api.user.MatrixUser interface RoomMemberModerationRenderer { @Composable fun Render( state: RoomMemberModerationState, - onSelectAction: (ModerationAction) -> Unit, + onSelectAction: (ModerationAction, MatrixUser) -> Unit, modifier: Modifier, ) } diff --git a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationState.kt b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationState.kt index b31857b594..51f791ed69 100644 --- a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationState.kt +++ b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationState.kt @@ -7,17 +7,20 @@ package io.element.android.features.roommembermoderation.api -import io.element.android.libraries.matrix.api.user.MatrixUser - interface RoomMemberModerationState { val canKick: Boolean val canBan: Boolean val eventSink: (RoomMemberModerationEvents) -> Unit } +data class ModerationActionState( + val action: ModerationAction, + val isEnabled: Boolean, +) + sealed interface ModerationAction { - data class DisplayProfile(val user: MatrixUser) : ModerationAction - data class KickUser(val user: MatrixUser) : ModerationAction - data class BanUser(val user: MatrixUser) : ModerationAction - data class UnbanUser(val user: MatrixUser) : ModerationAction + data object DisplayProfile : ModerationAction + data object KickUser : ModerationAction + data object BanUser : ModerationAction + data object UnbanUser : ModerationAction } diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/DefaultRoomMemberModerationRenderer.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/DefaultRoomMemberModerationRenderer.kt index 9a9cce3eb7..681a1eb733 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/DefaultRoomMemberModerationRenderer.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/DefaultRoomMemberModerationRenderer.kt @@ -15,6 +15,7 @@ import io.element.android.features.roommembermoderation.api.ModerationAction import io.element.android.features.roommembermoderation.api.RoomMemberModerationRenderer import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.user.MatrixUser import timber.log.Timber import javax.inject.Inject @@ -23,7 +24,7 @@ class DefaultRoomMemberModerationRenderer @Inject constructor() : RoomMemberMode @Composable override fun Render( state: RoomMemberModerationState, - onSelectAction: (ModerationAction) -> Unit, + onSelectAction: (ModerationAction, MatrixUser) -> Unit, modifier: Modifier ) { if (state is InternalRoomMemberModerationState) { diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationState.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationState.kt index 7e0c27a62d..b4bb8a3a27 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationState.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationState.kt @@ -8,6 +8,7 @@ package io.element.android.features.roommembermoderation.impl import io.element.android.features.roommembermoderation.api.ModerationAction +import io.element.android.features.roommembermoderation.api.ModerationActionState import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.architecture.AsyncAction @@ -20,7 +21,7 @@ data class InternalRoomMemberModerationState( override val canKick: Boolean, override val canBan: Boolean, val selectedUser: MatrixUser?, - val actions: ImmutableList, + val actions: ImmutableList, val kickUserAsyncAction: AsyncAction, val banUserAsyncAction: AsyncAction, val unbanUserAsyncAction: AsyncAction, diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt index fb9199ea54..0359a3120e 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt @@ -17,6 +17,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import im.vector.app.features.analytics.plan.RoomModeration import io.element.android.features.roommembermoderation.api.ModerationAction +import io.element.android.features.roommembermoderation.api.ModerationActionState import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.architecture.AsyncAction @@ -26,7 +27,8 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.RoomMember -import io.element.android.libraries.matrix.api.room.toMatrixUser +import io.element.android.libraries.matrix.api.room.RoomMembershipState +import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.room.canBanAsState import io.element.android.libraries.matrix.ui.room.canKickAsState @@ -64,38 +66,37 @@ class RoomMemberModerationPresenter @Inject constructor( var selectedUser by remember { mutableStateOf(null) } - val moderationActions = remember { mutableStateOf(persistentListOf()) } + val moderationActions = remember { mutableStateOf(persistentListOf()) } fun handleEvent(event: RoomMemberModerationEvents) { when (event) { is RoomMemberModerationEvents.ShowActionsForUser -> { coroutineScope.launch { selectedUser = event.user - moderationActions.value = persistentListOf(ModerationAction.DisplayProfile(event.user)) - room.getUpdatedMember(event.user.userId) - .onSuccess { - moderationActions.value = computeModerationActions( - member = it, - canKick = canKick.value, - canBan = canBan.value, - currentUserMemberPowerLevel = currentUserMemberPowerLevel.value, - ) - } + val member = room.membersStateFlow.value.roomMembers()?.firstOrNull { + it.userId == event.user.userId + } + moderationActions.value = computeModerationActions( + member = member, + canKick = canKick.value, + canBan = canBan.value, + currentUserMemberPowerLevel = currentUserMemberPowerLevel.value, + ) } } is RoomMemberModerationEvents.ProcessAction -> { when (val action = event.action) { is ModerationAction.DisplayProfile -> Unit is ModerationAction.KickUser -> { - selectedUser = action.user + selectedUser = event.targetUser kickUserAsyncAction.value = AsyncAction.ConfirmingNoParams } is ModerationAction.BanUser -> { - selectedUser = action.user + selectedUser = event.targetUser banUserAsyncAction.value = AsyncAction.ConfirmingNoParams } is ModerationAction.UnbanUser -> { - selectedUser = action.user + selectedUser = event.targetUser unbanUserAsyncAction.value = AsyncAction.ConfirmingNoParams } } @@ -112,18 +113,18 @@ class RoomMemberModerationPresenter @Inject constructor( } selectedUser = null } - is InternalRoomMemberModerationEvents.Reset -> { - selectedUser = null - kickUserAsyncAction.value = AsyncAction.Uninitialized - banUserAsyncAction.value = AsyncAction.Uninitialized - unbanUserAsyncAction.value = AsyncAction.Uninitialized - } is InternalRoomMemberModerationEvents.DoUnbanUser -> { selectedUser?.let { coroutineScope.unbanUser(it.userId, unbanUserAsyncAction) } selectedUser = null } + is InternalRoomMemberModerationEvents.Reset -> { + selectedUser = null + kickUserAsyncAction.value = AsyncAction.Uninitialized + banUserAsyncAction.value = AsyncAction.Uninitialized + unbanUserAsyncAction.value = AsyncAction.Uninitialized + } } } @@ -140,20 +141,27 @@ class RoomMemberModerationPresenter @Inject constructor( } private fun computeModerationActions( - member: RoomMember, + member: RoomMember?, canKick: Boolean, canBan: Boolean, currentUserMemberPowerLevel: Long, - ): PersistentList { - val memberAsUser = member.toMatrixUser() + ): PersistentList { return buildList { - add(ModerationAction.DisplayProfile(memberAsUser)) - val canModerateThisUser = member.powerLevel < currentUserMemberPowerLevel && member.membership.isActive() - if (canKick && canModerateThisUser) { - add(ModerationAction.KickUser(memberAsUser)) + add(ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true)) + // Assume the member is a regular user when it's unknown + val canModerateThisUser = (member?.powerLevel ?: 0) < currentUserMemberPowerLevel + // Assume the member is joined when it's unknown + val membership = member?.membership ?: RoomMembershipState.JOIN + if (canKick) { + val isKickEnabled = canModerateThisUser && membership.isActive() + add(ModerationActionState(action = ModerationAction.KickUser, isEnabled = isKickEnabled)) } - if (canBan && canModerateThisUser) { - add(ModerationAction.BanUser(memberAsUser)) + if (canBan) { + if (membership == RoomMembershipState.BAN) { + add(ModerationActionState(action = ModerationAction.UnbanUser, isEnabled = canModerateThisUser)) + } else { + add(ModerationActionState(action = ModerationAction.BanUser, isEnabled = canModerateThisUser)) + } } }.toPersistentList() } @@ -204,5 +212,4 @@ class RoomMemberModerationPresenter @Inject constructor( } } } - } diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationStateProvider.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationStateProvider.kt index 9d52eec503..cdfca792d6 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationStateProvider.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationStateProvider.kt @@ -9,6 +9,7 @@ package io.element.android.features.roommembermoderation.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.roommembermoderation.api.ModerationAction +import io.element.android.features.roommembermoderation.api.ModerationActionState import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData @@ -25,24 +26,32 @@ class RoomMemberModerationStateProvider : PreviewParameterProvider = emptyList(), + actions: List = emptyList(), kickUserAsyncAction: AsyncAction = AsyncAction.Uninitialized, banUserAsyncAction: AsyncAction = AsyncAction.Uninitialized, unbanUserAsyncAction: AsyncAction = AsyncAction.Uninitialized, diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt index 31b474057d..a2a86e4d13 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt @@ -31,6 +31,7 @@ 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.roommembermoderation.api.ModerationAction +import io.element.android.features.roommembermoderation.api.ModerationActionState 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 @@ -58,7 +59,7 @@ import timber.log.Timber @Composable fun RoomMemberModerationView( state: InternalRoomMemberModerationState, - onSelectAction: (ModerationAction) -> Unit, + onSelectAction: (ModerationAction, MatrixUser) -> Unit, modifier: Modifier = Modifier, ) { Box(modifier = modifier) { @@ -201,8 +202,8 @@ private fun RoomMemberAsyncActions( @Composable private fun RoomMemberActionsBottomSheet( user: MatrixUser, - actions: ImmutableList, - onSelectAction: (ModerationAction) -> Unit, + actions: ImmutableList, + onSelectAction: (ModerationAction, MatrixUser) -> Unit, onDismiss: () -> Unit, ) { val coroutineScope = rememberCoroutineScope() @@ -223,8 +224,8 @@ private fun RoomMemberActionsBottomSheet( Avatar( avatarData = user.getAvatarData(size = AvatarSize.RoomListManageUser), modifier = Modifier - .padding(bottom = 28.dp) - .align(Alignment.CenterHorizontally) + .padding(bottom = 28.dp) + .align(Alignment.CenterHorizontally) ) user.displayName?.let { Text( @@ -234,8 +235,8 @@ private fun RoomMemberActionsBottomSheet( overflow = TextOverflow.Ellipsis, textAlign = TextAlign.Center, modifier = Modifier - .padding(start = 16.dp, end = 16.dp, bottom = 8.dp) - .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, bottom = 8.dp) + .fillMaxWidth() ) } Text( @@ -246,35 +247,38 @@ private fun RoomMemberActionsBottomSheet( overflow = TextOverflow.Ellipsis, textAlign = TextAlign.Center, modifier = Modifier - .padding(horizontal = 16.dp) - .fillMaxWidth() + .padding(horizontal = 16.dp) + .fillMaxWidth() ) Spacer(modifier = Modifier.height(32.dp)) - for (action in actions) { - when (action) { + for (actionState in actions) { + when (val action = actionState.action) { is ModerationAction.DisplayProfile -> { ListItem( headlineContent = { Text(stringResource(R.string.screen_bottom_sheet_manage_room_member_member_user_info)) }, leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Info())), onClick = { coroutineScope.launch { - onSelectAction(action) + onSelectAction(action, user) bottomSheetState.hide() } - } + }, + enabled = actionState.isEnabled ) } is ModerationAction.KickUser -> { ListItem( headlineContent = { Text(stringResource(R.string.screen_bottom_sheet_manage_room_member_remove)) }, - leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Block())), + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Close())), + style = ListItemStyle.Destructive, onClick = { coroutineScope.launch { bottomSheetState.hide() - onSelectAction(action) + onSelectAction(action, user) } - } + }, + enabled = actionState.isEnabled ) } is ModerationAction.BanUser -> { @@ -285,12 +289,26 @@ private fun RoomMemberActionsBottomSheet( onClick = { coroutineScope.launch { bottomSheetState.hide() - onSelectAction(action) + onSelectAction(action, user) } - } + }, + enabled = actionState.isEnabled + ) + } + is ModerationAction.UnbanUser -> { + ListItem( + headlineContent = { Text(stringResource(R.string.screen_room_member_list_manage_member_unban_action)) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Restart())), + style = ListItemStyle.Destructive, + onClick = { + coroutineScope.launch { + bottomSheetState.hide() + onSelectAction(action, user) + } + }, + enabled = actionState.isEnabled ) } - is ModerationAction.UnbanUser -> Unit } } } @@ -303,12 +321,14 @@ internal fun RoomMembersModerationViewPreview(@PreviewParameter(RoomMemberModera ElementPreview { Box( modifier = Modifier - .fillMaxWidth() - .heightIn(min = 64.dp) + .fillMaxWidth() + .heightIn(min = 64.dp) ) { RoomMemberModerationView( state = state, - onSelectAction = {}, + onSelectAction = { _, _ -> + + }, ) } } From 0b6c5964a1dd2f0fd89e0ae9de7283d2a052d37b Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 19 May 2025 22:16:17 +0200 Subject: [PATCH 05/50] change (member moderation) : fix all existing tests --- .../messages/impl/MessagesPresenterTest.kt | 5 + .../messages/impl/MessagesViewTest.kt | 55 +-- .../pinned/list/PinnedMessagesListViewTest.kt | 3 +- .../impl/timeline/TimelineViewTest.kt | 3 +- .../impl/members/RoomMemberListNode.kt | 3 +- .../impl/members/RoomMemberListPresenter.kt | 11 +- .../members/RoomMemberListPresenterTest.kt | 74 ++-- .../RoomMembersModerationPresenterTest.kt | 351 ------------------ .../RoomMembersModerationViewTest.kt | 274 -------------- .../impl/InternalRoomMemberModerationState.kt | 3 - ...ernalRoomMemberModerationStateProvider.kt} | 6 +- .../impl/RoomMemberModerationView.kt | 2 +- 12 files changed, 64 insertions(+), 726 deletions(-) delete mode 100644 features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenterTest.kt delete mode 100644 features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationViewTest.kt rename features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/{RoomMemberModerationStateProvider.kt => InternalRoomMemberModerationStateProvider.kt} (91%) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index 2a3c34feee..c21431e2ea 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -35,6 +35,7 @@ import io.element.android.features.messages.impl.timeline.protection.aTimelinePr import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessageComposerState import io.element.android.features.messages.test.timeline.FakeHtmlConverterProvider import io.element.android.features.roomcall.api.aStandByCallState +import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter @@ -1182,6 +1183,9 @@ class MessagesPresenterTest { textEditorState = aTextEditorStateMarkdown(initialText = "", initialFocus = false) ) }, + roomMemberModerationPresenter: Presenter = Presenter { + aRoomMemberModerationState() + }, encryptionService: FakeEncryptionService = FakeEncryptionService(), actionListEventSink: (ActionListEvents) -> Unit = {}, ): MessagesPresenter { @@ -1199,6 +1203,7 @@ class MessagesPresenterTest { linkPresenter = { aLinkState() }, pinnedMessagesBannerPresenter = { aLoadedPinnedMessagesBannerState() }, roomCallStatePresenter = { aStandByCallState() }, + roomMemberModerationPresenter = roomMemberModerationPresenter, syncService = FakeSyncService(), snackbarDispatcher = SnackbarDispatcher(), navigator = navigator, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt index 5080013d27..b0d22ad4c1 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt @@ -53,6 +53,9 @@ import io.element.android.features.messages.impl.timeline.components.receipt.bot import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.timeline.item.event.getAvatarUrl +import io.element.android.libraries.matrix.api.timeline.item.event.getDisplayName +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.ui.strings.CommonStrings @@ -310,40 +313,42 @@ class MessagesViewTest { @Test @Config(qualifiers = "h1024dp") - fun `clicking on the avatar of the sender of an Event invoke expected callback`() { - val eventsRecorder = EventsRecorder(expectEvents = false) + fun `clicking on the avatar of the sender of an Event emits the expected event`() { + val eventsRecorder = EventsRecorder() val state = aMessagesState( eventSink = eventsRecorder ) - val timelineItem = state.timelineState.timelineItems.first() - ensureCalledOnceWithParam( - param = (timelineItem as TimelineItem.Event).senderId - ) { callback -> - rule.setMessagesView( - state = state, - onUserDataClick = callback, + val timelineEvent = state.timelineState.timelineItems.filterIsInstance().first() + rule.setMessagesView(state = state) + rule.onNodeWithTag(TestTags.timelineItemSenderAvatar.value, useUnmergedTree = true).performClick() + eventsRecorder.assertSingle( + MessagesEvents.OnUserClicked( + MatrixUser( + userId = timelineEvent.senderId, + displayName = timelineEvent.senderProfile.getDisplayName(), + avatarUrl = timelineEvent.senderProfile.getAvatarUrl() + ) ) - rule.onNodeWithTag(TestTags.timelineItemSenderAvatar.value, useUnmergedTree = true).performClick() - } + ) } @Test @Config(qualifiers = "h1024dp") - fun `clicking on the display name of the sender of an Event invoke expected callback`() { - val eventsRecorder = EventsRecorder(expectEvents = false) - val state = aMessagesState( - eventSink = eventsRecorder - ) - val timelineItem = state.timelineState.timelineItems.first() - ensureCalledOnceWithParam( - param = (timelineItem as TimelineItem.Event).senderId - ) { callback -> - rule.setMessagesView( - state = state, - onUserDataClick = callback, + fun `clicking on the display name of the sender of an Event emits expected event`() { + val eventsRecorder = EventsRecorder() + val state = aMessagesState(eventSink = eventsRecorder) + val timelineEvent = state.timelineState.timelineItems.filterIsInstance().first() + rule.setMessagesView(state = state) + rule.onNodeWithTag(TestTags.timelineItemSenderAvatar.value, useUnmergedTree = true).performClick() + eventsRecorder.assertSingle( + MessagesEvents.OnUserClicked( + MatrixUser( + userId = timelineEvent.senderId, + displayName = timelineEvent.senderProfile.getDisplayName(), + avatarUrl = timelineEvent.senderProfile.getAvatarUrl() + ) ) - rule.onNodeWithTag(TestTags.timelineItemSenderName.value, useUnmergedTree = true).performClick() - } + ) } @Test diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt index 35ac685303..0200da24e2 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt @@ -22,6 +22,7 @@ import io.element.android.features.messages.impl.timeline.aTimelineItemList import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemFileContent import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EventsRecorder @@ -99,7 +100,7 @@ private fun AndroidComposeTestRule.setPinne state: PinnedMessagesListState, onBackClick: () -> Unit = EnsureNeverCalled(), onEventClick: (event: TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(), - onUserDataClick: (UserId) -> Unit = EnsureNeverCalledWithParam(), + onUserDataClick: (MatrixUser) -> Unit = EnsureNeverCalledWithParam(), onLinkClick: (Link) -> Unit = EnsureNeverCalledWithParam(), onLinkLongClick: (Link) -> Unit = EnsureNeverCalledWithParam(), ) { diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt index 208c0ab140..178523cf64 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt @@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.api.core.UniqueId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalledWithParam @@ -175,7 +176,7 @@ class TimelineViewTest { private fun AndroidComposeTestRule.setTimelineView( state: TimelineState, timelineProtectionState: TimelineProtectionState = aTimelineProtectionState(), - onUserDataClick: (UserId) -> Unit = EnsureNeverCalledWithParam(), + onUserDataClick: (MatrixUser) -> Unit = EnsureNeverCalledWithParam(), onLinkClick: (Link) -> Unit = EnsureNeverCalledWithParam(), onMessageClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(), onMessageLongClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(), 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 cf2f89c41e..3d7fecd0ae 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 @@ -29,7 +29,7 @@ import io.element.android.services.analytics.api.AnalyticsService class RoomMemberListNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - presenterFactory: RoomMemberListPresenter.Factory, + private val presenter: RoomMemberListPresenter, private val analyticsService: AnalyticsService, private val roomMemberModerationRenderer: RoomMemberModerationRenderer, ) : Node(buildContext, plugins = plugins), RoomMemberListNavigator { @@ -39,7 +39,6 @@ class RoomMemberListNode @AssistedInject constructor( } private val callbacks = plugins() - private val presenter = presenterFactory.create(this) init { lifecycle.subscribe( 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 c637f338cb..4de57787c8 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 @@ -16,8 +16,6 @@ import androidx.compose.runtime.produceState import androidx.compose.runtime.remember 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.roommembermoderation.api.ModerationAction import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents @@ -53,12 +51,7 @@ class RoomMemberListPresenter @AssistedInject constructor( private val coroutineDispatchers: CoroutineDispatchers, private val roomMembersModerationPresenter: Presenter, private val encryptionService: EncryptionService, - @Assisted private val navigator: RoomMemberListNavigator, ) : Presenter { - @AssistedFactory - interface Factory { - fun create(navigator: RoomMemberListNavigator): RoomMemberListPresenter - } @Composable override fun present(): RoomMemberListState { @@ -168,10 +161,8 @@ class RoomMemberListPresenter @AssistedInject constructor( is RoomMemberListEvents.RoomMemberSelected -> if (event.roomMember.membership == RoomMembershipState.BAN) { roomModerationState.eventSink(RoomMemberModerationEvents.ProcessAction(ModerationAction.UnbanUser, event.roomMember.toMatrixUser())) - } else if (!isDm.value && (roomModerationState.canBan || roomModerationState.canKick)) { - roomModerationState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(event.roomMember.toMatrixUser())) } else { - navigator.openRoomMemberDetails(event.roomMember.userId) + roomModerationState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(event.roomMember.toMatrixUser())) } } } diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenterTest.kt index 82e5a910dc..52173f43ff 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenterTest.kt @@ -11,9 +11,8 @@ 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.moderation.RoomMembersModerationEvents -import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationState -import io.element.android.features.roomdetails.impl.members.moderation.aRoomMembersModerationState +import io.element.android.features.roommembermoderation.api.RoomMemberModerationState +import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.api.core.UserId @@ -24,7 +23,6 @@ import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.aRoomInfo -import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -42,12 +40,12 @@ class RoomMemberListPresenterTest { fun `member loading is done automatically on start, but is async`() = runTest { val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - updateMembersResult = { Result.success(Unit) }, - canInviteResult = { Result.success(true) } - ).apply { - // Needed to avoid discarding the loaded members as a partial and invalid result - givenRoomInfo(aRoomInfo(joinedMembersCount = 2)) - } + updateMembersResult = { Result.success(Unit) }, + canInviteResult = { Result.success(true) } + ).apply { + // Needed to avoid discarding the loaded members as a partial and invalid result + givenRoomInfo(aRoomInfo(joinedMembersCount = 2)) + } ) val presenter = createPresenter(joinedRoom = room) moleculeFlow(RecompositionMode.Immediate) { @@ -97,9 +95,9 @@ class RoomMemberListPresenterTest { val presenter = createPresenter( joinedRoom = FakeJoinedRoom( baseRoom = FakeBaseRoom( - updateMembersResult = { Result.success(Unit) }, - canInviteResult = { Result.success(true) } - ) + updateMembersResult = { Result.success(Unit) }, + canInviteResult = { Result.success(true) } + ) ) ) moleculeFlow(RecompositionMode.Immediate) { @@ -204,12 +202,12 @@ class RoomMemberListPresenterTest { } @Test - fun `present - RoomMemberSelected by default opens the room member details through the navigator`() = runTest { - val navigator = FakeRoomMemberListNavigator() - val roomMembersModerationStateLambda = { aRoomMembersModerationState(canDisplayModerationActions = false) } + fun `present - RoomMemberSelected will open the moderation options when target user is not banned`() = runTest { + val roomMemberModerationPresenter= Presenter { + aRoomMemberModerationState(canBan = true, canKick = true) + } val presenter = createPresenter( - roomMembersModerationStateLambda = roomMembersModerationStateLambda, - navigator = navigator, + roomMemberModerationPresenter = roomMemberModerationPresenter, joinedRoom = FakeJoinedRoom( baseRoom = FakeBaseRoom( updateMembersResult = { Result.success(Unit) }, @@ -222,36 +220,6 @@ class RoomMemberListPresenterTest { }.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() - val eventsRecorder = EventsRecorder() - val roomMembersModerationStateLambda = { - aRoomMembersModerationState( - canDisplayModerationActions = true, - eventSink = eventsRecorder, - ) - } - val presenter = createPresenter( - roomMembersModerationStateLambda = roomMembersModerationStateLambda, - navigator = navigator, - joinedRoom = FakeJoinedRoom( - baseRoom = FakeBaseRoom( - updateMembersResult = { Result.success(Unit) }, - canInviteResult = { Result.success(true) } - ) - ) - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - skipItems(1) - awaitItem().eventSink(RoomMemberListEvents.RoomMemberSelected(aVictor())) - eventsRecorder.assertSingle(RoomMembersModerationEvents.SelectRoomMember(aVictor())) } } } @@ -277,19 +245,19 @@ private fun TestScope.createDataSource( private fun TestScope.createPresenter( coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), joinedRoom: JoinedRoom = FakeJoinedRoom( - baseRoom = FakeBaseRoom( + baseRoom = FakeBaseRoom( updateMembersResult = { Result.success(Unit) } ) ), roomMemberListDataSource: RoomMemberListDataSource = createDataSource(coroutineDispatchers = coroutineDispatchers), - roomMembersModerationStateLambda: () -> RoomMembersModerationState = { aRoomMembersModerationState() }, encryptedService: FakeEncryptionService = FakeEncryptionService(), - navigator: RoomMemberListNavigator = object : RoomMemberListNavigator {} + roomMemberModerationPresenter: Presenter = Presenter { + aRoomMemberModerationState() + }, ) = RoomMemberListPresenter( room = joinedRoom, roomMemberListDataSource = roomMemberListDataSource, coroutineDispatchers = coroutineDispatchers, - roomMembersModerationPresenter = { roomMembersModerationStateLambda() }, + roomMembersModerationPresenter = roomMemberModerationPresenter, encryptionService = encryptedService, - navigator = navigator, ) diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenterTest.kt deleted file mode 100644 index 00239fdf82..0000000000 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenterTest.kt +++ /dev/null @@ -1,351 +0,0 @@ -/* - * Copyright 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.roomdetails.impl.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 im.vector.app.features.analytics.plan.RoomModeration -import io.element.android.features.roomdetails.impl.aJoinedRoom -import io.element.android.features.roomdetails.impl.members.aRoomMember -import io.element.android.features.roomdetails.impl.members.aVictor -import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.core.coroutine.CoroutineDispatchers -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.RoomMembersState -import io.element.android.libraries.matrix.api.room.RoomMembershipState -import io.element.android.libraries.matrix.test.A_REASON -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.FakeJoinedRoom -import io.element.android.libraries.matrix.test.room.aRoomInfo -import io.element.android.services.analytics.test.FakeAnalyticsService -import io.element.android.tests.testutils.lambda.lambdaRecorder -import io.element.android.tests.testutils.lambda.value -import io.element.android.tests.testutils.test -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 RoomMembersModerationPresenterTest { - @Test - fun `canDisplayModerationActions - when room is DM is false`() = runTest { - val room = aJoinedRoom( - isPublic = true, - activeMemberCount = 2, - canKickResult = { Result.success(true) }, - canBanResult = { Result.success(true) }, - userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, - ).apply { - givenRoomInfo(aRoomInfo(isDirect = true, activeMembersCount = 2)) - } - val presenter = createRoomMembersModerationPresenter(joinedRoom = room) - presenter.test { - assertThat(awaitItem().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 = aJoinedRoom( - activeMemberCount = 10, - canKickResult = { Result.success(true) }, - canBanResult = { Result.success(true) }, - userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, - ) - val presenter = createRoomMembersModerationPresenter(joinedRoom = room) - presenter.test { - skipItems(1) - assertThat(awaitItem().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 = aJoinedRoom( - activeMemberCount = 10, - canKickResult = { Result.success(true) }, - canBanResult = { Result.success(true) }, - userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, - ) - val presenter = createRoomMembersModerationPresenter(joinedRoom = room) - presenter.test { - skipItems(1) - assertThat(awaitItem().canDisplayModerationActions).isTrue() - } - } - - @Test - fun `present - SelectRoomMember when the current user has permissions displays member actions`() = runTest { - val room = aJoinedRoom( - canKickResult = { Result.success(true) }, - canBanResult = { Result.success(true) }, - userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, - ) - val selectedMember = aVictor() - val presenter = createRoomMembersModerationPresenter(joinedRoom = 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 = aJoinedRoom( - sessionId = A_USER_ID, - canKickResult = { Result.success(true) }, - canBanResult = { Result.success(true) }, - userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, - ) - val selectedMember = aRoomMember(A_USER_ID_2, powerLevel = 100L) - val presenter = createRoomMembersModerationPresenter(joinedRoom = 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 = aJoinedRoom( - canKickResult = { Result.success(true) }, - canBanResult = { Result.success(true) }, - userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, - ) - val presenter = createRoomMembersModerationPresenter(joinedRoom = room) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - skipItems(1) - awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember)) - with(awaitItem()) { - assertThat(selectedRoomMember).isNull() - assertThat(unbanUserAsyncAction).isEqualTo(ConfirmingRoomMemberAction(selectedMember)) - } - } - } - - @Test - fun `present - Kick requires confirmation and then kicks the user`() = runTest { - val analyticsService = FakeAnalyticsService() - val kickUserResult = lambdaRecorder> { _, _ -> Result.success(Unit) } - val room = aJoinedRoom( - canKickResult = { Result.success(true) }, - canBanResult = { Result.success(true) }, - userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, - kickUserResult = kickUserResult, - ) - val selectedMember = aVictor() - val presenter = createRoomMembersModerationPresenter(joinedRoom = room, analyticsService = analyticsService) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - skipItems(1) - awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember)) - awaitItem().eventSink(RoomMembersModerationEvents.KickUser) - val confirmingState = awaitItem() - assertThat(confirmingState.kickUserAsyncAction).isEqualTo(AsyncAction.ConfirmingNoParams) - // Confirm - confirmingState.eventSink(RoomMembersModerationEvents.DoKickUser(reason = A_REASON)) - skipItems(1) - val loadingState = awaitItem() - assertThat(loadingState.actions).isEmpty() - assertThat(loadingState.kickUserAsyncAction).isEqualTo(AsyncAction.Loading) - with(awaitItem()) { - assertThat(kickUserAsyncAction).isEqualTo(AsyncAction.Success(Unit)) - assertThat(selectedRoomMember).isNull() - } - assertThat(analyticsService.capturedEvents.last()).isEqualTo(RoomModeration(RoomModeration.Action.KickMember)) - kickUserResult.assertions().isCalledOnce().with( - value(selectedMember.userId), - value(A_REASON), - ) - } - } - - @Test - fun `present - BanUser requires confirmation and then bans the user`() = runTest { - val analyticsService = FakeAnalyticsService() - val banUserResult = lambdaRecorder> { _, _ -> Result.success(Unit) } - val room = aJoinedRoom( - canKickResult = { Result.success(true) }, - canBanResult = { Result.success(true) }, - userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, - banUserResult = banUserResult, - ) - val selectedMember = aVictor() - val presenter = createRoomMembersModerationPresenter(joinedRoom = room, analyticsService = analyticsService) - 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.ConfirmingNoParams) - // Confirm - confirmingState.eventSink(RoomMembersModerationEvents.DoBanUser(reason = A_REASON)) - skipItems(1) - val loadingItem = awaitItem() - assertThat(loadingItem.actions).isEmpty() - assertThat(loadingItem.selectedRoomMember).isNull() - assertThat(loadingItem.banUserAsyncAction).isEqualTo(AsyncAction.Loading) - with(awaitItem()) { - assertThat(banUserAsyncAction).isEqualTo(AsyncAction.Success(Unit)) - assertThat(selectedRoomMember).isNull() - } - assertThat(analyticsService.capturedEvents.last()).isEqualTo(RoomModeration(RoomModeration.Action.BanMember)) - banUserResult.assertions().isCalledOnce().with( - value(selectedMember.userId), - value(A_REASON), - ) - } - } - - @Test - fun `present - UnbanUser requires confirmation and then unbans the user`() = runTest { - val analyticsService = FakeAnalyticsService() - val selectedMember = aRoomMember(A_USER_ID_2, membership = RoomMembershipState.BAN) - val room = aJoinedRoom( - canKickResult = { Result.success(true) }, - canBanResult = { Result.success(true) }, - userRoleResult = { Result.success(RoomMember.Role.ADMIN) }, - unBanUserResult = { _, _ -> Result.success(Unit) }, - ).apply { - givenRoomMembersState(RoomMembersState.Ready(persistentListOf(selectedMember))) - } - val presenter = createRoomMembersModerationPresenter(joinedRoom = room, analyticsService = analyticsService) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - skipItems(1) - // Displays unban confirmation dialog - awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember)) - val confirmingState = awaitItem() - assertThat(confirmingState.selectedRoomMember).isNull() - assertThat(confirmingState.actions).isEmpty() - assertThat(confirmingState.unbanUserAsyncAction).isEqualTo(ConfirmingRoomMemberAction(selectedMember)) - // Confirms unban - confirmingState.eventSink(RoomMembersModerationEvents.UnbanUser(selectedMember.userId)) - assertThat(awaitItem().unbanUserAsyncAction).isEqualTo(AsyncAction.Loading) - with(awaitItem()) { - assertThat(unbanUserAsyncAction).isEqualTo(AsyncAction.Success(Unit)) - assertThat(selectedRoomMember).isNull() - } - assertThat(analyticsService.capturedEvents.last()).isEqualTo(RoomModeration(RoomModeration.Action.UnbanMember)) - } - } - - @Test - fun `present - Reset removes the selected user and actions`() = runTest { - val room = aJoinedRoom( - canKickResult = { Result.success(true) }, - canBanResult = { Result.success(true) }, - userRoleResult = { Result.success(RoomMember.Role.USER) }, - ) - val presenter = createRoomMembersModerationPresenter(joinedRoom = room) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - skipItems(1) - // Select a user - awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(aVictor())) - // Reset state - awaitItem().eventSink(RoomMembersModerationEvents.Reset) - val finalItem = awaitItem() - assertThat(finalItem.selectedRoomMember).isNull() - assertThat(finalItem.actions).isEmpty() - } - } - - @Test - fun `present - Reset resets any async actions`() = runTest { - val room = aJoinedRoom( - canKickResult = { Result.success(true) }, - canBanResult = { Result.success(true) }, - kickUserResult = { _, _ -> Result.failure(Throwable("Eek")) }, - banUserResult = { _, _ -> Result.failure(Throwable("Eek")) }, - unBanUserResult = { _, _ -> Result.failure(Throwable("Eek")) }, - userRoleResult = { Result.success(RoomMember.Role.USER) }, - ) - val presenter = createRoomMembersModerationPresenter(joinedRoom = room) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialItem = awaitItem() - // Kick user and fail - awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(aVictor())) - awaitItem().eventSink(RoomMembersModerationEvents.DoKickUser(reason = "")) - skipItems(1) - 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.DoBanUser(reason = "")) - skipItems(1) - 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(aVictor().userId)) - 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.createRoomMembersModerationPresenter( - joinedRoom: FakeJoinedRoom = aJoinedRoom(), - dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), - analyticsService: FakeAnalyticsService = FakeAnalyticsService(), - ): RoomMembersModerationPresenter { - return RoomMembersModerationPresenter( - room = joinedRoom, - dispatchers = dispatchers, - analyticsService = analyticsService, - ) - } -} diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationViewTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationViewTest.kt deleted file mode 100644 index b501c808c7..0000000000 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationViewTest.kt +++ /dev/null @@ -1,274 +0,0 @@ -/* - * Copyright 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.roomdetails.impl.members.moderation - -import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performTextInput -import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.element.android.features.roomdetails.impl.R -import io.element.android.features.roomdetails.impl.members.anAlice -import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.test.A_REASON -import io.element.android.libraries.ui.strings.CommonStrings -import io.element.android.tests.testutils.EnsureNeverCalledWithParam -import io.element.android.tests.testutils.EventsRecorder -import io.element.android.tests.testutils.clickOn -import io.element.android.tests.testutils.ensureCalledOnceWithParam -import io.element.android.tests.testutils.pressBackKey -import org.junit.Ignore -import org.junit.Rule -import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith -import org.robolectric.annotation.Config - -@RunWith(AndroidJUnit4::class) -class RoomMembersModerationViewTest { - @get:Rule val rule = createAndroidComposeRule() - - @Ignore("This test is not passing yet, need to investigate") - @Test - fun `clicking on back emits the expected event`() { - val eventsRecorder = EventsRecorder() - val roomMember = anAlice() - val state = aRoomMembersModerationState( - selectedRoomMember = roomMember, - actions = listOf( - ModerationAction.DisplayProfile(roomMember.userId), - ), - eventSink = eventsRecorder - ) - rule.setRoomMembersModerationView( - state = state, - ) - rule.pressBackKey() - // Give time for the bottom sheet to animate - rule.mainClock.advanceTimeBy(1_000) - eventsRecorder.assertSingle(RoomMembersModerationEvents.Reset) - } - - @Test - fun `clicking on 'See user info' invokes the expected callback`() { - val eventsRecorder = EventsRecorder(expectEvents = false) - val roomMember = anAlice() - val state = aRoomMembersModerationState( - selectedRoomMember = roomMember, - actions = listOf( - ModerationAction.DisplayProfile(roomMember.userId), - ), - eventSink = eventsRecorder - ) - ensureCalledOnceWithParam(roomMember.userId) { callback -> - rule.setRoomMembersModerationView( - state = state, - onDisplayMemberProfile = callback - ) - rule.clickOn(CommonStrings.screen_bottom_sheet_manage_room_member_member_user_info) - } - } - - @Config(qualifiers = "h1024dp") - @Test - fun `clicking on 'Remove member' emits the expected event`() { - val eventsRecorder = EventsRecorder() - val roomMember = anAlice() - val state = aRoomMembersModerationState( - selectedRoomMember = roomMember, - actions = listOf( - ModerationAction.DisplayProfile(roomMember.userId), - ModerationAction.KickUser(roomMember.userId), - ), - eventSink = eventsRecorder - ) - rule.setRoomMembersModerationView( - state = state, - ) - rule.clickOn(CommonStrings.screen_bottom_sheet_manage_room_member_remove) - // Give time for the bottom sheet to animate - rule.mainClock.advanceTimeBy(1_000) - eventsRecorder.assertSingle(RoomMembersModerationEvents.KickUser) - } - - @Test - fun `cancelling 'Remove member' confirmation emits the expected event`() { - val eventsRecorder = EventsRecorder() - val roomMember = anAlice() - val state = aRoomMembersModerationState( - selectedRoomMember = roomMember, - kickUserAsyncAction = AsyncAction.ConfirmingNoParams, - eventSink = eventsRecorder - ) - rule.setRoomMembersModerationView( - state = state, - ) - // Note: the string key semantics is not perfect here :/ - rule.clickOn(CommonStrings.action_cancel) - eventsRecorder.assertSingle(RoomMembersModerationEvents.Reset) - } - - @Test - fun `confirming 'Remove member' reason edition then validation emits the expected event`() { - val eventsRecorder = EventsRecorder() - val roomMember = anAlice() - val state = aRoomMembersModerationState( - selectedRoomMember = roomMember, - kickUserAsyncAction = AsyncAction.ConfirmingNoParams, - eventSink = eventsRecorder - ) - rule.setRoomMembersModerationView( - state = state, - ) - val reason = rule.activity.getString(CommonStrings.common_reason) - rule.onNodeWithText(reason).performTextInput(A_REASON) - rule.clickOn(CommonStrings.screen_bottom_sheet_manage_room_member_kick_member_confirmation_action) - eventsRecorder.assertSingle(RoomMembersModerationEvents.DoKickUser(reason = A_REASON)) - } - - @Test - fun `confirming 'Remove member' confirmation emits the expected event`() { - val eventsRecorder = EventsRecorder() - val roomMember = anAlice() - val state = aRoomMembersModerationState( - selectedRoomMember = roomMember, - kickUserAsyncAction = AsyncAction.ConfirmingNoParams, - eventSink = eventsRecorder - ) - rule.setRoomMembersModerationView( - state = state, - ) - // Note: the string key semantics is not perfect here :/ - rule.clickOn(CommonStrings.screen_bottom_sheet_manage_room_member_kick_member_confirmation_action) - eventsRecorder.assertSingle(RoomMembersModerationEvents.DoKickUser(reason = "")) - } - - @Config(qualifiers = "h1024dp") - @Test - fun `clicking on 'Remove and ban member' emits the expected event`() { - val eventsRecorder = EventsRecorder() - val roomMember = anAlice() - val state = aRoomMembersModerationState( - selectedRoomMember = roomMember, - actions = listOf( - ModerationAction.DisplayProfile(roomMember.userId), - ModerationAction.KickUser(roomMember.userId), - ModerationAction.BanUser(roomMember.userId), - ), - eventSink = eventsRecorder - ) - rule.setRoomMembersModerationView( - state = state, - ) - // Note: the string key semantics is not perfect here :/ - rule.clickOn(R.string.screen_room_member_list_manage_member_remove_confirmation_ban) - // Give time for the bottom sheet to animate - rule.mainClock.advanceTimeBy(1_000) - eventsRecorder.assertSingle(RoomMembersModerationEvents.BanUser) - } - - @Test - fun `cancelling 'Remove and ban member' confirmation emits the expected event`() { - val eventsRecorder = EventsRecorder() - val roomMember = anAlice() - val state = aRoomMembersModerationState( - selectedRoomMember = roomMember, - banUserAsyncAction = AsyncAction.ConfirmingNoParams, - eventSink = eventsRecorder - ) - rule.setRoomMembersModerationView( - state = state, - ) - // Note: the string key semantics is not perfect here :/ - rule.clickOn(CommonStrings.action_cancel) - eventsRecorder.assertSingle(RoomMembersModerationEvents.Reset) - } - - @Test - fun `confirming 'Remove and ban member' reason edition emits the expected event`() { - val eventsRecorder = EventsRecorder() - val roomMember = anAlice() - val state = aRoomMembersModerationState( - selectedRoomMember = roomMember, - banUserAsyncAction = AsyncAction.ConfirmingNoParams, - eventSink = eventsRecorder - ) - rule.setRoomMembersModerationView( - state = state, - ) - val reason = rule.activity.getString(CommonStrings.common_reason) - rule.onNodeWithText(reason).performTextInput(A_REASON) - rule.clickOn(CommonStrings.screen_bottom_sheet_manage_room_member_ban_member_confirmation_action) - eventsRecorder.assertSingle(RoomMembersModerationEvents.DoBanUser(reason = A_REASON)) - } - - @Test - fun `confirming 'Remove and ban member' confirmation emits the expected event`() { - val eventsRecorder = EventsRecorder() - val roomMember = anAlice() - val state = aRoomMembersModerationState( - selectedRoomMember = roomMember, - banUserAsyncAction = AsyncAction.ConfirmingNoParams, - eventSink = eventsRecorder - ) - rule.setRoomMembersModerationView( - state = state, - ) - // Note: the string key semantics is not perfect here :/ - rule.clickOn(CommonStrings.screen_bottom_sheet_manage_room_member_ban_member_confirmation_action) - eventsRecorder.assertSingle(RoomMembersModerationEvents.DoBanUser(reason = "")) - } - - @Test - fun `cancelling 'Unban member' confirmation emits the expected event`() { - val eventsRecorder = EventsRecorder() - val roomMember = anAlice() - val state = aRoomMembersModerationState( - selectedRoomMember = roomMember, - unbanUserAsyncAction = ConfirmingRoomMemberAction(roomMember), - eventSink = eventsRecorder - ) - rule.setRoomMembersModerationView( - state = state, - ) - // Note: the string key semantics is not perfect here :/ - rule.clickOn(CommonStrings.action_cancel) - eventsRecorder.assertSingle(RoomMembersModerationEvents.Reset) - } - - @Test - fun `confirming 'Unban member' confirmation emits the expected event`() { - val eventsRecorder = EventsRecorder() - val roomMember = anAlice() - val state = aRoomMembersModerationState( - selectedRoomMember = roomMember, - unbanUserAsyncAction = ConfirmingRoomMemberAction(roomMember), - eventSink = eventsRecorder - ) - rule.setRoomMembersModerationView( - state = state, - ) - // Note: the string key semantics is not perfect here :/ - rule.clickOn(R.string.screen_room_member_list_manage_member_unban_action) - eventsRecorder.assertSingle(RoomMembersModerationEvents.UnbanUser(roomMember.userId)) - } -} - -private fun AndroidComposeTestRule.setRoomMembersModerationView( - state: RoomMembersModerationState, - onDisplayMemberProfile: (UserId) -> Unit = EnsureNeverCalledWithParam() -) { - setContent { - RoomMembersModerationView( - state = state, - onDisplayMemberProfile = onDisplayMemberProfile, - ) - } -} diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationState.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationState.kt index b4bb8a3a27..86c816970c 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationState.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationState.kt @@ -7,13 +7,10 @@ package io.element.android.features.roommembermoderation.impl -import io.element.android.features.roommembermoderation.api.ModerationAction import io.element.android.features.roommembermoderation.api.ModerationActionState import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.user.MatrixUser import kotlinx.collections.immutable.ImmutableList diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationStateProvider.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationStateProvider.kt similarity index 91% rename from features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationStateProvider.kt rename to features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationStateProvider.kt index cdfca792d6..44f8bdc056 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationStateProvider.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationStateProvider.kt @@ -12,15 +12,11 @@ import io.element.android.features.roommembermoderation.api.ModerationAction import io.element.android.features.roommembermoderation.api.ModerationActionState import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.architecture.AsyncData 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.RoomMembershipState -import io.element.android.libraries.matrix.api.room.toMatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser import kotlinx.collections.immutable.toPersistentList -class RoomMemberModerationStateProvider : PreviewParameterProvider { +class InternalRoomMemberModerationStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aRoomMembersModerationState( diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt index a2a86e4d13..7f285e1f76 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt @@ -317,7 +317,7 @@ private fun RoomMemberActionsBottomSheet( @PreviewsDayNight @Composable -internal fun RoomMembersModerationViewPreview(@PreviewParameter(RoomMemberModerationStateProvider::class) state: InternalRoomMemberModerationState) { +internal fun RoomMemberModerationViewPreview(@PreviewParameter(InternalRoomMemberModerationStateProvider::class) state: InternalRoomMemberModerationState) { ElementPreview { Box( modifier = Modifier From 50161930d685a188733c6877d4ff9aed698dfca5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 20 May 2025 19:24:18 +0000 Subject: [PATCH 06/50] fix(deps): update dependency com.google.firebase:firebase-bom to v33.14.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0aa2f926c7..12cab6324e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -72,7 +72,7 @@ kover_gradle_plugin = { module = "org.jetbrains.kotlinx:kover-gradle-plugin", ve ksp_gradle_plugin = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" } gms_google_services = "com.google.gms:google-services:4.4.2" # https://firebase.google.com/docs/android/setup#available-libraries -google_firebase_bom = "com.google.firebase:firebase-bom:33.13.0" +google_firebase_bom = "com.google.firebase:firebase-bom:33.14.0" firebase_appdistribution_gradle = { module = "com.google.firebase:firebase-appdistribution-gradle", version.ref = "firebaseAppDistribution" } autonomousapps_dependencyanalysis_plugin = { module = "com.autonomousapps:dependency-analysis-gradle-plugin", version.ref = "dependencyAnalysis" } ksp_plugin = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" } From 538a8441cdd96cab064d2da5e9e7b79106fe1f90 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 21 May 2025 18:18:42 +0200 Subject: [PATCH 07/50] Changelog for version 25.05.4 --- CHANGES.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 04a7fa528e..b06cdd6603 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,50 @@ + + +Changes in Element X v25.05.4 +============================= + +Rust SDK: https://github.com/matrix-org/matrix-rust-sdk/releases/tag/matrix-sdk-ffi%2F20250521 + +## What's Changed +### 🙌 Improvements +* Change (report room) : check if server supports the report room api by @ganfra in https://github.com/element-hq/element-x-android/pull/4718 +### 🐛 Bugfixes +* Improve audio focus management by @bmarty in https://github.com/element-hq/element-x-android/pull/4707 +* When transcoding a video fails, send it as a file by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4257 +* Disable mutliple click (parallel or serial) on a room by @bmarty in https://github.com/element-hq/element-x-android/pull/4683 +* Fix generic mime type used when externally sharing several files by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4715 +* Fix issues on JoinedRoom / BaseRoom by @bmarty in https://github.com/element-hq/element-x-android/pull/4724 +* Use the right live timeline instance in `RustRoomFactory` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4745 +### 🗣 Translations +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/4739 +### 🧱 Build +* Ensure the CI is marked as failed when Maestro test is failing by @bmarty in https://github.com/element-hq/element-x-android/pull/4700 +* Trigger pipeline build when a release tag is pushed by @bmarty in https://github.com/element-hq/element-x-android/pull/4741 +* Fix compilation issues. by @bmarty in https://github.com/element-hq/element-x-android/pull/4750 +### 📄 Documentation +* README.md: fix broken link by @richvdh in https://github.com/element-hq/element-x-android/pull/4728 +### Dependency upgrades +* chore(config): migrate renovate config by @renovate in https://github.com/element-hq/element-x-android/pull/4688 +* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.5.13 by @renovate in https://github.com/element-hq/element-x-android/pull/4716 +* fix(deps): update dependency io.sentry:sentry-android to v8.12.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4717 +* chore(deps): update plugin sonarqube to v6.2.0.5505 by @renovate in https://github.com/element-hq/element-x-android/pull/4725 +* fix(deps): update dependency com.posthog:posthog-android to v3.15.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4723 +* fix(deps): update dependency com.squareup.retrofit2:retrofit-bom to v2.12.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4727 +* chore(deps): update codecov/codecov-action action to v5.4.3 by @renovate in https://github.com/element-hq/element-x-android/pull/4730 +* fix(deps): update kotlin by @renovate in https://github.com/element-hq/element-x-android/pull/4713 +* fix(deps): update dependency com.squareup.retrofit2:retrofit-bom to v3 by @renovate in https://github.com/element-hq/element-x-android/pull/4729 +* fix(deps): update kotlinpoet to v2.2.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4732 +* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.5.21 by @renovate in https://github.com/element-hq/element-x-android/pull/4759 +### Others +* Remove event cache feature flag by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4719 +* Check homeserver when login using qr code by @bmarty in https://github.com/element-hq/element-x-android/pull/4708 +* Merge on boarding module to login module by @bmarty in https://github.com/element-hq/element-x-android/pull/4746 +* Allow configuration to provide multiple account providers. by @bmarty in https://github.com/element-hq/element-x-android/pull/4742 +* Reduce API of JoinedRoom, caller must use the Timeline API from liveTimeline instead by @bmarty in https://github.com/element-hq/element-x-android/pull/4731 + + +**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.05.3...v25.05.4 + Changes in Element X v25.05.3 ============================= From 60c41551084698f6947ba33d8ace727dcc0e2dcd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 21 May 2025 18:19:24 +0200 Subject: [PATCH 08/50] fix(deps): update datastore to v1.1.7 (#4754) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e55a7baf07..fbd8ac9e68 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,7 +11,7 @@ firebaseAppDistribution = "5.1.1" # AndroidX core = "1.16.0" -datastore = "1.1.6" +datastore = "1.1.7" constraintlayout = "2.2.1" constraintlayout_compose = "1.1.1" lifecycle = "2.9.0" From 5a2aeac6b6e87807c9cd91585f6fe36ed5255207 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 21 May 2025 18:19:42 +0200 Subject: [PATCH 09/50] Add support for login link (#4752) * Add support for login link https://mobile.element.io/element?account_provider=example.org&login_hint=mxid:@alice:example.org * Update screenshots * Reduce code duplication * Add test on OnBoardingPresenter * Fix tool * Ignore login parameter if user is not allowed to connect to the provided server. * Improve tests. * Cleanup * Revert change on Project.xml. * Add documentation * Improve LoginHelper * Rename LoginFlow to LoginMode Move LoginFlow to package io.element.android.features.login.impl.login Rename some implementation of LoginMode Rename LoginFlowView to LoginModeView * Change launchMode of MainActivity from `singleTop` to `singleTask` Using launchMode singleTask to avoid multiple instances of the Activity when the app is already open. This is important for incoming share and for opening the application from a mobile.element.io link. Closes #4074 --------- Co-authored-by: ElementBot --- app/src/main/AndroidManifest.xml | 26 ++- appnav/build.gradle.kts | 1 + .../android/appnav/NotLoggedInFlowNode.kt | 15 ++ .../io/element/android/appnav/RootFlowNode.kt | 43 ++++- .../android/appnav/intent/IntentResolver.kt | 15 +- .../appnav/intent/IntentResolverTest.kt | 29 +++- .../features/login/api/LoginEntryPoint.kt | 6 + .../features/login/api/LoginIntentResolver.kt | 12 ++ .../android/features/login/api/LoginParams.kt | 21 +++ features/login/impl/build.gradle.kts | 1 + .../login/impl/DefaultLoginEntryPoint.kt | 8 + .../login/impl/DefaultLoginIntentResolver.kt | 30 ++++ .../features/login/impl/LoginFlowNode.kt | 52 ++++-- .../features/login/impl/login/LoginHelper.kt | 119 ++++++++++++++ .../features/login/impl/login/LoginMode.kt | 16 ++ .../login/impl/login/LoginModeView.kt | 87 ++++++++++ .../impl/onboarding/OnBoardingPresenter.kt | 44 ------ .../login/impl/onboarding/OnBoardingState.kt | 15 -- .../ConfirmAccountProviderPresenter.kt | 104 ++---------- .../ConfirmAccountProviderState.kt | 12 +- .../ConfirmAccountProviderStateProvider.kt | 7 +- .../ConfirmAccountProviderView.kt | 61 ++----- .../screens/onboarding/OnBoardingEvents.kt | 16 ++ .../onboarding/OnBoardingNode.kt | 40 ++++- .../screens/onboarding/OnBoardingPresenter.kt | 77 +++++++++ .../screens/onboarding/OnBoardingState.kt | 24 +++ .../onboarding/OnBoardingStateProvider.kt | 11 +- .../onboarding/OnBoardingView.kt | 60 ++++++- .../impl/DefaultLoginIntentResolverTest.kt | 81 ++++++++++ .../onboarding/OnBoardingPresenterTest.kt | 68 -------- .../ConfirmAccountProviderPresenterTest.kt | 83 +++++----- .../onboarding/OnBoardingPresenterTest.kt | 149 ++++++++++++++++++ .../onboarding/OnboardingViewTest.kt | 44 +++++- features/login/test/build.gradle.kts | 19 +++ .../login/test/FakeLoginIntentResolver.kt | 20 +++ .../api/auth/MatrixAuthenticationService.kt | 5 +- .../auth/RustMatrixAuthenticationService.kt | 7 +- .../android/libraries/matrix/test/TestData.kt | 4 +- .../auth/FakeMatrixAuthenticationService.kt | 5 +- ...ns.onboarding_OnBoardingView_Day_0_en.png} | 0 ...ns.onboarding_OnBoardingView_Day_1_en.png} | 0 ...ns.onboarding_OnBoardingView_Day_2_en.png} | 0 ...ns.onboarding_OnBoardingView_Day_3_en.png} | 0 ...ns.onboarding_OnBoardingView_Day_4_en.png} | 0 ...ens.onboarding_OnBoardingView_Day_5_en.png | 3 + ....onboarding_OnBoardingView_Night_0_en.png} | 0 ....onboarding_OnBoardingView_Night_1_en.png} | 0 ....onboarding_OnBoardingView_Night_2_en.png} | 0 ....onboarding_OnBoardingView_Night_3_en.png} | 0 ....onboarding_OnBoardingView_Night_4_en.png} | 0 ...s.onboarding_OnBoardingView_Night_5_en.png | 3 + tools/adb/deeplink_mobile.sh | 12 ++ 52 files changed, 1092 insertions(+), 363 deletions(-) create mode 100644 features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginIntentResolver.kt create mode 100644 features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginParams.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/DefaultLoginIntentResolver.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginHelper.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginMode.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeView.kt delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/onboarding/OnBoardingPresenter.kt delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/onboarding/OnBoardingState.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingEvents.kt rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{ => screens}/onboarding/OnBoardingNode.kt (55%) create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{ => screens}/onboarding/OnBoardingStateProvider.kt (67%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{ => screens}/onboarding/OnBoardingView.kt (75%) create mode 100644 features/login/impl/src/test/kotlin/io/element/android/features/login/impl/DefaultLoginIntentResolverTest.kt delete mode 100644 features/login/impl/src/test/kotlin/io/element/android/features/login/impl/onboarding/OnBoardingPresenterTest.kt create mode 100644 features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt rename features/login/impl/src/test/kotlin/io/element/android/features/login/impl/{ => screens}/onboarding/OnboardingViewTest.kt (67%) create mode 100644 features/login/test/build.gradle.kts create mode 100644 features/login/test/src/main/kotlin/io/element/android/features/login/test/FakeLoginIntentResolver.kt rename tests/uitests/src/test/snapshots/images/{features.login.impl.onboarding_OnBoardingView_Day_0_en.png => features.login.impl.screens.onboarding_OnBoardingView_Day_0_en.png} (100%) rename tests/uitests/src/test/snapshots/images/{features.login.impl.onboarding_OnBoardingView_Day_1_en.png => features.login.impl.screens.onboarding_OnBoardingView_Day_1_en.png} (100%) rename tests/uitests/src/test/snapshots/images/{features.login.impl.onboarding_OnBoardingView_Day_2_en.png => features.login.impl.screens.onboarding_OnBoardingView_Day_2_en.png} (100%) rename tests/uitests/src/test/snapshots/images/{features.login.impl.onboarding_OnBoardingView_Day_3_en.png => features.login.impl.screens.onboarding_OnBoardingView_Day_3_en.png} (100%) rename tests/uitests/src/test/snapshots/images/{features.login.impl.onboarding_OnBoardingView_Day_4_en.png => features.login.impl.screens.onboarding_OnBoardingView_Day_4_en.png} (100%) create mode 100644 tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_5_en.png rename tests/uitests/src/test/snapshots/images/{features.login.impl.onboarding_OnBoardingView_Night_0_en.png => features.login.impl.screens.onboarding_OnBoardingView_Night_0_en.png} (100%) rename tests/uitests/src/test/snapshots/images/{features.login.impl.onboarding_OnBoardingView_Night_1_en.png => features.login.impl.screens.onboarding_OnBoardingView_Night_1_en.png} (100%) rename tests/uitests/src/test/snapshots/images/{features.login.impl.onboarding_OnBoardingView_Night_2_en.png => features.login.impl.screens.onboarding_OnBoardingView_Night_2_en.png} (100%) rename tests/uitests/src/test/snapshots/images/{features.login.impl.onboarding_OnBoardingView_Night_3_en.png => features.login.impl.screens.onboarding_OnBoardingView_Night_3_en.png} (100%) rename tests/uitests/src/test/snapshots/images/{features.login.impl.onboarding_OnBoardingView_Night_4_en.png => features.login.impl.screens.onboarding_OnBoardingView_Night_4_en.png} (100%) create mode 100644 tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_5_en.png create mode 100755 tools/adb/deeplink_mobile.sh diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c26c9a5a51..6619603f93 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -34,11 +34,17 @@ android:value='androidx.startup' /> + @@ -54,6 +60,9 @@ android:host="open" android:scheme="elementx" /> + @@ -80,6 +89,21 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml index ae76014f3b..d11b4203fe 100644 --- a/app/src/main/res/xml/data_extraction_rules.xml +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -9,9 +9,18 @@ --> - + + + + + + - + + + + + - + \ No newline at end of file From e02053d85ec664bf4a2a42fee78f92749233934e Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Mon, 2 Jun 2025 14:28:46 +0200 Subject: [PATCH 40/50] Make sure HeaderFooterPage can contents be scrolled (#4704) Co-authored-by: ElementBot --- .../enter/SecureBackupEnterRecoveryKeyView.kt | 36 ++++++++++++++++++- .../designsystem/atomic/pages/FlowStepPage.kt | 3 ++ .../atomic/pages/HeaderFooterPage.kt | 10 ++++-- ...ytics.impl_AnalyticsOptInView_Day_0_en.png | 4 +-- ...ytics.impl_AnalyticsOptInView_Day_1_en.png | 4 +-- ...ics.impl_AnalyticsOptInView_Night_0_en.png | 4 +-- ...ics.impl_AnalyticsOptInView_Night_1_en.png | 4 +-- ...ations_NotificationsOptInView_Day_0_en.png | 4 +-- ...ions_NotificationsOptInView_Night_0_en.png | 4 +-- ...s.joinroom.impl_JoinRoomView_Day_13_en.png | 4 +-- ...joinroom.impl_JoinRoomView_Night_13_en.png | 4 +-- ...ns.qrcode.scan_QrCodeScanView_Day_0_en.png | 4 +-- ...ns.qrcode.scan_QrCodeScanView_Day_1_en.png | 4 +-- ...ns.qrcode.scan_QrCodeScanView_Day_2_en.png | 4 +-- ...ns.qrcode.scan_QrCodeScanView_Day_3_en.png | 4 +-- ....qrcode.scan_QrCodeScanView_Night_0_en.png | 4 +-- ....qrcode.scan_QrCodeScanView_Night_1_en.png | 4 +-- ....qrcode.scan_QrCodeScanView_Night_2_en.png | 4 +-- ....qrcode.scan_QrCodeScanView_Night_3_en.png | 4 +-- ...et.root_ResetIdentityRootView_Day_0_en.png | 4 +-- ...et.root_ResetIdentityRootView_Day_1_en.png | 4 +-- ....root_ResetIdentityRootView_Night_0_en.png | 4 +-- ....root_ResetIdentityRootView_Night_1_en.png | 4 +-- ...p_SecureBackupSetupViewChange_Day_2_en.png | 4 +-- ...p_SecureBackupSetupViewChange_Day_3_en.png | 4 +-- ...p_SecureBackupSetupViewChange_Day_4_en.png | 4 +-- ...SecureBackupSetupViewChange_Night_2_en.png | 4 +-- ...SecureBackupSetupViewChange_Night_3_en.png | 4 +-- ...SecureBackupSetupViewChange_Night_4_en.png | 4 +-- ...l.setup_SecureBackupSetupView_Day_2_en.png | 4 +-- ...l.setup_SecureBackupSetupView_Day_3_en.png | 4 +-- ...l.setup_SecureBackupSetupView_Day_4_en.png | 4 +-- ...setup_SecureBackupSetupView_Night_2_en.png | 4 +-- ...setup_SecureBackupSetupView_Night_3_en.png | 4 +-- ...setup_SecureBackupSetupView_Night_4_en.png | 4 +-- ....signedout.impl_SignedOutView_Day_0_en.png | 4 +-- ...ignedout.impl_SignedOutView_Night_0_en.png | 4 +-- ...ming_IncomingVerificationView_Day_5_en.png | 4 +-- ...ming_IncomingVerificationView_Day_6_en.png | 4 +-- ...ming_IncomingVerificationView_Day_7_en.png | 4 +-- ...ming_IncomingVerificationView_Day_8_en.png | 4 +-- ...ming_IncomingVerificationView_Day_9_en.png | 4 +-- ...ng_IncomingVerificationView_Night_5_en.png | 4 +-- ...ng_IncomingVerificationView_Night_6_en.png | 4 +-- ...ng_IncomingVerificationView_Night_7_en.png | 4 +-- ...ng_IncomingVerificationView_Night_8_en.png | 4 +-- ...ng_IncomingVerificationView_Night_9_en.png | 4 +-- ...oing_OutgoingVerificationView_Day_4_en.png | 4 +-- ...oing_OutgoingVerificationView_Day_5_en.png | 4 +-- ...oing_OutgoingVerificationView_Day_6_en.png | 4 +-- ...oing_OutgoingVerificationView_Day_9_en.png | 4 +-- ...ng_OutgoingVerificationView_Night_4_en.png | 4 +-- ...ng_OutgoingVerificationView_Night_5_en.png | 4 +-- ...ng_OutgoingVerificationView_Night_6_en.png | 4 +-- ...ng_OutgoingVerificationView_Night_9_en.png | 4 +-- ...tem.atomic.pages_FlowStepPage_Day_0_en.png | 4 +-- ...m.atomic.pages_FlowStepPage_Night_0_en.png | 4 +-- 57 files changed, 153 insertions(+), 112 deletions(-) diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyView.kt index b533763735..ed5a381754 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyView.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyView.kt @@ -7,11 +7,24 @@ package io.element.android.features.securebackup.impl.enter +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.isImeVisible import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.relocation.BringIntoViewRequester +import androidx.compose.foundation.relocation.bringIntoViewRequester import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -25,6 +38,9 @@ import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlin.time.Duration.Companion.milliseconds @Composable fun SecureBackupEnterRecoveryKeyView( @@ -55,12 +71,30 @@ fun SecureBackupEnterRecoveryKeyView( } } +@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class) @Composable private fun Content( state: SecureBackupEnterRecoveryKeyState, ) { + val bringIntoViewRequester = remember { BringIntoViewRequester() } + var isFocused by remember { mutableStateOf(false) } + val isImeVisible = WindowInsets.isImeVisible + val coroutineScope = rememberCoroutineScope() + LaunchedEffect(isImeVisible, isFocused) { + // When the keyboard is shown, we want to scroll the text field into view + if (isImeVisible && isFocused) { + coroutineScope.launch { + // Delay to ensure the keyboard is fully shown + delay(100.milliseconds) + bringIntoViewRequester.bringIntoView() + } + } + } RecoveryKeyView( - modifier = Modifier.padding(top = 52.dp, bottom = 32.dp), + modifier = Modifier + .onFocusChanged { isFocused = it.isFocused } + .bringIntoViewRequester(bringIntoViewRequester) + .padding(top = 52.dp, bottom = 32.dp), state = state.recoveryKeyViewState, onClick = null, onChange = { diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/FlowStepPage.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/FlowStepPage.kt index 4a7c3f2dd5..bf549ecda2 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/FlowStepPage.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/FlowStepPage.kt @@ -13,9 +13,11 @@ import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons @@ -63,6 +65,7 @@ fun FlowStepPage( } }, title = {}, + colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent) ) }, header = { diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/HeaderFooterPage.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/HeaderFooterPage.kt index c059cfe880..5100abad64 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/HeaderFooterPage.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/HeaderFooterPage.kt @@ -9,12 +9,14 @@ package io.element.android.libraries.designsystem.atomic.pages import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState @@ -94,17 +96,19 @@ fun HeaderFooterPage( .run { if (isScrollable) { verticalScroll(rememberScrollState()) + // Make sure the scrollable content takes the full available height + .height(IntrinsicSize.Max) } else { Modifier } } // Apply insets here so if the content is scrollable it can get below the top app bar if needed .padding(contentInsetsPadding) - .weight(1f), + .weight(1f, fill = true), ) { // Header header() - Box(modifier = Modifier.weight(1f)) { + Box { content() } } @@ -112,7 +116,7 @@ fun HeaderFooterPage( // Footer Box( modifier = Modifier - .padding(horizontal = 16.dp) + .padding(start = 16.dp, end = 16.dp, top = 16.dp) .fillMaxWidth() .padding(footerInsetsPadding) ) { diff --git a/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Day_0_en.png index e8e0bbf1d8..65e4ae3fcb 100644 --- a/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:255709a0c6b3ffe337e5a3ce657e350539d9278a2a1c7994ac40752a0bffecff -size 82449 +oid sha256:236ed0a010eb4ad4ca28b474a269fe919dc2a329132c42f02092a31bdf7d84a1 +size 83154 diff --git a/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Day_1_en.png index 4ca3de0a14..7e1db079dc 100644 --- a/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:232fcbe898588450577fca29090873626c2df7cc4afba91a87d3956d29f0afbc -size 81150 +oid sha256:39a70a38c06be96a7d9acee023b3d26fb03b374a981185e5c2a2d217fdbed1fb +size 81825 diff --git a/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Night_0_en.png index d794cb4425..fcec6752c8 100644 --- a/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:82812d497f41b7e34d00cf2fadbde22115200539fcb25660dd9f9c9378093f0e -size 75610 +oid sha256:6b29e98607174bd4f8c64e582c8e012a95565a97601ade537375cf0515585592 +size 76228 diff --git a/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Night_1_en.png index c3a8af5c2d..778259739b 100644 --- a/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:26f4a151850c4c21ed57d0a1d36ea72aece585ed5ec6d54303edbeaa91d06df9 -size 73959 +oid sha256:f10da703d90ac9bc99c7145eea93598e209f1c674fc918ce69df8875bddbf736 +size 74353 diff --git a/tests/uitests/src/test/snapshots/images/features.ftue.impl.notifications_NotificationsOptInView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.ftue.impl.notifications_NotificationsOptInView_Day_0_en.png index ec56a647c0..94e57d26f7 100644 --- a/tests/uitests/src/test/snapshots/images/features.ftue.impl.notifications_NotificationsOptInView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.ftue.impl.notifications_NotificationsOptInView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3b5294950885cb417983db822b7615dfd0c4988c0eef7eceb466b751c0834020 -size 68101 +oid sha256:e6aa9ee5920f74d5a5fef6e2a0c81ef8e0480a428161a33283c55dfeb17d39ef +size 68963 diff --git a/tests/uitests/src/test/snapshots/images/features.ftue.impl.notifications_NotificationsOptInView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.ftue.impl.notifications_NotificationsOptInView_Night_0_en.png index 3f29a09d02..f68b07b3fa 100644 --- a/tests/uitests/src/test/snapshots/images/features.ftue.impl.notifications_NotificationsOptInView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.ftue.impl.notifications_NotificationsOptInView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c6ea578f8aa88e306f50407c48cc08243e147635bd5e94f13c4f054ff02ed380 -size 59199 +oid sha256:f36c7ded0fb9e543dd367bc7ae676463d36cd3aa449af8eeffe98966a96dd847 +size 59960 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_13_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_13_en.png index 19aa34ce60..6f8e9c095b 100644 --- a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_13_en.png +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_13_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fbdd42f8092b3bf8fc58ea012972a77444bff2200585fc4d9e0765453e98c251 -size 29425 +oid sha256:fc93b58449602518d7fd6e14334bba0793a5692f478367e825b75c6ca9de685a +size 29451 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_13_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_13_en.png index 656133b3fb..1e702f7ec9 100644 --- a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_13_en.png +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_13_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c35203dccee8ae4c1f7257d54ce84394fcc1b433a1dbd3ca66f8989eb776edf1 -size 28789 +oid sha256:8cbdba568da3e23a836524fbde2d431268d6ce27408a3490fa3bf1407a2862e0 +size 28729 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_0_en.png index 77f7df9ebf..dc56c9ef07 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d98408bf12d027294336036bc931a71e8445ea64fbe24382e28f5c9ab571e2f -size 14771 +oid sha256:ee07c8d10b22fde6ba769ffb9f4165dc361a736f406462971e675fe49f937c4f +size 14601 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_1_en.png index cd1f9e9d8b..876adb14ad 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe810ba7c5afd8c6453c0798dfd57dbdb32ef3db486a2346b391616c67384ab7 -size 19865 +oid sha256:82d2a63343a1888fea014a660839b258e384dedb5172878f50513543fac908a1 +size 19675 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_2_en.png index 1c308c53d7..a2a2133bd5 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d46119857cf8c5ae5f08306e70ff1da7bbf2439f1823cd0d4dae7543ebf117c3 -size 26383 +oid sha256:6926b4809939097a505ce1864dabcf5ca50461269b89118154b5e692317aec9f +size 26204 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_3_en.png index 15ac598a95..a23153e5c1 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e6ff5f8f48d4976f3765816f81b540d7100599cf536613c47e39e00ab5c87cbb -size 32549 +oid sha256:0cf8d0e215067d3c6bab9f7e7669ca4639b896f7dc418631dd9959fb99452ca7 +size 32385 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_0_en.png index 7c5bc9bc71..758b702ea0 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d133a357d0de55f21240136477f7e1f878195b77e60aa555f8ffb1c39799a5a3 -size 14055 +oid sha256:cb9fc1376b91a4aa5a591100bf98fa15a138d26f5d43ee22c48a61a89943ff9f +size 13974 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_1_en.png index 987a058c54..7c76229aef 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e9c1deea2ce90a44dac812217fa8f49f290bae023146b883dac9ae3040a4242f -size 18969 +oid sha256:bf7a6a0f41ee301fb203c25c4636d7ce96d15e76eb536c7573e7792657dcd459 +size 18892 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_2_en.png index b0a00983e7..ec5323d6d4 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:daf8aeeb87942d65602cb24d4c58a4165a43cacc7c5c98f8032e28551bb71845 -size 25245 +oid sha256:3f1929531fee3dc938cda1f865260c83569cd5980ba5da5b7a0b3b6285085aed +size 25160 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_3_en.png index 6b25807c82..f0985eeea4 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:35e7dd1dd561f3e2a32b55732d81054036703957a93186a24664423bb7343454 -size 31212 +oid sha256:e77dc96ffab18d00cdced2486407f3d70d0213a9f2937672c12e6d920364b17e +size 31129 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en.png index 5b439e89c9..1799aeace0 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d0a50e8f002b7866579ad058d1d71621e85c00282cca4b5ddb1ecd5d3fb0bfcd -size 57245 +oid sha256:d49a60535fa260b6a920fe1765b0a4e6277a4a513eba5a35f2992f69576bf121 +size 63448 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en.png index c477c9e6fa..9cf51a3a69 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:91759024a0698ea4d4fd05fada6125fae5f86978d26e86d3cb088f57424772cc -size 49470 +oid sha256:45471081e7c479fec9279dcba2e8a65f283bde610c006f387581280a489dec1c +size 54422 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en.png index 6bc5c9c9b4..1fccc9e974 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:db0eb360bed04c1169be840fa15429a180b9812b0f38b903639de70cc0eb6369 -size 56370 +oid sha256:b235d43a2d760c978c2fad9ca3cb6d9c84991563b70bb79852da00ddb1a04d6f +size 62362 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en.png index 9557e5e9f4..a283c16bb0 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:11213aea7ceb0eab52a27ecff95075e3b15508bcfeca04bad88326cbb3989f9f -size 47362 +oid sha256:310bf9cce0143135a94b16704fca319db26607976c2d78bfc2896f85b67c62bb +size 52199 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_2_en.png index 985895ddcd..277f7990b2 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6cdf434a3f63224c6d164f6e153d846b26644b270a63df01808ac9577883d7fe -size 56501 +oid sha256:3d5aec2ca7aa00c9a3b4a21e6589497b784d90676ffe94999efd245916de2f8d +size 54310 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_3_en.png index 985895ddcd..277f7990b2 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6cdf434a3f63224c6d164f6e153d846b26644b270a63df01808ac9577883d7fe -size 56501 +oid sha256:3d5aec2ca7aa00c9a3b4a21e6589497b784d90676ffe94999efd245916de2f8d +size 54310 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_4_en.png index 8ca4fafeb2..9ec14d711f 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eeb6b4d6b8ae5e34c9621302dc04bde9086d3857f1928d360d16f64c24e8bb3e -size 50287 +oid sha256:56d784b2a9bd8c36845ad0d559ccb0fc38d93b281efa9f50590be375177ecc52 +size 48492 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_2_en.png index 3bf6a1df65..b145bb8019 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:05e2c93413dfef1dfbad559ea4a7321ce3cec076d05d448e28474bde66e3fff4 -size 54646 +oid sha256:fd0e7f92b0a307db669342b7880f14fbc2adc0b70a6cd7445771af95a9386f9e +size 52397 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_3_en.png index 3bf6a1df65..b145bb8019 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:05e2c93413dfef1dfbad559ea4a7321ce3cec076d05d448e28474bde66e3fff4 -size 54646 +oid sha256:fd0e7f92b0a307db669342b7880f14fbc2adc0b70a6cd7445771af95a9386f9e +size 52397 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_4_en.png index b10498328a..a72ef4b673 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:24fa75a0c3fcdc535bc96a3bce8dc2edd9fbcfd9cb3663f7b454394611bef94b -size 47571 +oid sha256:16fc4591af9f0fc19aa2aa6d2e9d44cf87176797a8f9540605ec265744f9fb28 +size 45864 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_2_en.png index 985895ddcd..277f7990b2 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6cdf434a3f63224c6d164f6e153d846b26644b270a63df01808ac9577883d7fe -size 56501 +oid sha256:3d5aec2ca7aa00c9a3b4a21e6589497b784d90676ffe94999efd245916de2f8d +size 54310 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_3_en.png index 985895ddcd..277f7990b2 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6cdf434a3f63224c6d164f6e153d846b26644b270a63df01808ac9577883d7fe -size 56501 +oid sha256:3d5aec2ca7aa00c9a3b4a21e6589497b784d90676ffe94999efd245916de2f8d +size 54310 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_4_en.png index 8ca4fafeb2..9ec14d711f 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eeb6b4d6b8ae5e34c9621302dc04bde9086d3857f1928d360d16f64c24e8bb3e -size 50287 +oid sha256:56d784b2a9bd8c36845ad0d559ccb0fc38d93b281efa9f50590be375177ecc52 +size 48492 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_2_en.png index 3bf6a1df65..b145bb8019 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:05e2c93413dfef1dfbad559ea4a7321ce3cec076d05d448e28474bde66e3fff4 -size 54646 +oid sha256:fd0e7f92b0a307db669342b7880f14fbc2adc0b70a6cd7445771af95a9386f9e +size 52397 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_3_en.png index 3bf6a1df65..b145bb8019 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:05e2c93413dfef1dfbad559ea4a7321ce3cec076d05d448e28474bde66e3fff4 -size 54646 +oid sha256:fd0e7f92b0a307db669342b7880f14fbc2adc0b70a6cd7445771af95a9386f9e +size 52397 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_4_en.png index b10498328a..a72ef4b673 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:24fa75a0c3fcdc535bc96a3bce8dc2edd9fbcfd9cb3663f7b454394611bef94b -size 47571 +oid sha256:16fc4591af9f0fc19aa2aa6d2e9d44cf87176797a8f9540605ec265744f9fb28 +size 45864 diff --git a/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Day_0_en.png index 4cddd8f758..287773510d 100644 --- a/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ec003b496dd46394a0d7e261ca7d1f29c67539bb342ffc63c70f29429a4c9383 -size 56106 +oid sha256:fbad1b9aeb93d87860738982345be39484cd5fdc8cc6dd2772f5b21580d7f75f +size 55827 diff --git a/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Night_0_en.png index 508765ed60..df29965580 100644 --- a/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:04e95499c071791b6abe8f22c29ec81f21f92b913ae24965f6455c8bcff3ac86 -size 55164 +oid sha256:5e7c5fdb23b2e6406e07cc518f03e6e49acae68f7b91dc2626ea7c34e1875a57 +size 54829 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_5_en.png index 3bd25e8667..6ec6fd79b0 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:89bbdbeef7d2d331e95907a4c1b9d34764c3ea39b8bb63a4f42fb210515de75b -size 46713 +oid sha256:88f616a97acfe8c98705843ebbe5222d8d7068c64345a63594bff10b27b56e0b +size 46485 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_6_en.png index 115ae57f89..888bb98b6a 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a71a88e65140d150cbe50d116e13119dcb9bd96191832ac482f423f284c55246 -size 47048 +oid sha256:79fe88714a2e3d9a738de0e457220170716adf88e85eeef8e3a460facacac8ee +size 46823 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_7_en.png index 7bd143c02c..7217061a08 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:531de4a36c2fd9b52e7f29fcde221bd8897178bbfb7cbf5b5640acbee4439d1a -size 40396 +oid sha256:0e42383bf5dda6a4511cb915c40f76fee54670158c848d4ee1b125c57eea290b +size 40166 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_8_en.png index bfbc350682..5d6ce8a817 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74b41a474f1c26e668b05c9b9da5313739fc23e2679d7f58d17326499fd8a8f3 -size 40733 +oid sha256:ef4fa260df2194e4deefd4e3d9d57779afcbbc6cd56ddb4d699afa6fbac94cd8 +size 40502 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_9_en.png index 32d51c2a7c..7a8af23c30 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9cc1a6ed77d4bdca2154e6a98294a970071f48bbd986bfb175a5ed0f0360d3a2 -size 31534 +oid sha256:a599d61446db401ad156139758917b54f702d765d24f20fce246674e6dd05cc3 +size 31555 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_5_en.png index 85af305006..8382812e13 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc7264ee36a36ac79f9c52ab24f2cdccecf149aee3b36f1f03121b5a98ac7aa8 -size 45556 +oid sha256:fddc479ceef787049b0e4af3ce61f6efbf67f7dccd81c3d6c17fcc7cea9c1127 +size 45383 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_6_en.png index ff0e6ef380..a37c7355fd 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:adbb4b7a7297bf7e586d663dc665cedc2f7dba0e841a3f93d09d3be27e3527fd -size 45919 +oid sha256:16bcb5d1d66aa3b5bb8e574e67f47e87c4beee9cc8795c4ac24ea68c57d32dbb +size 45750 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_7_en.png index c98ce28904..07b3b6b822 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d1f6072fb62c959e9685f915c3985c58b9bcc2c630d6bc9a4370f4b2d88a374c -size 39645 +oid sha256:8f99c623fc13c336ee3138741275a4fd42677d92d8357e37d6b9ae4cb75b22e1 +size 39450 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_8_en.png index 6e06243a24..c5a310b668 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:82460b85ca35f30990ab76a78e635e6783f8ce39524e3f9fd6ffe5b7caab0a14 -size 40012 +oid sha256:c8be233702d5b478db8358053da7cfc3712028f1b0b31287b839cb2a2dab3866 +size 39831 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_9_en.png index 5326db7e73..b0e8833345 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f92d9cf0b0ea928325a8e64ffe32bd0484f9cb4130cd9815a2ea6997e2d5852c -size 30675 +oid sha256:093b5ba8be120cf9f93cd3448aac328c1a8ba191d45e48f514f12879e9d3d5ae +size 30702 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_4_en.png index 3bd25e8667..6ec6fd79b0 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:89bbdbeef7d2d331e95907a4c1b9d34764c3ea39b8bb63a4f42fb210515de75b -size 46713 +oid sha256:88f616a97acfe8c98705843ebbe5222d8d7068c64345a63594bff10b27b56e0b +size 46485 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_5_en.png index 115ae57f89..888bb98b6a 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a71a88e65140d150cbe50d116e13119dcb9bd96191832ac482f423f284c55246 -size 47048 +oid sha256:79fe88714a2e3d9a738de0e457220170716adf88e85eeef8e3a460facacac8ee +size 46823 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_6_en.png index 7bd143c02c..7217061a08 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:531de4a36c2fd9b52e7f29fcde221bd8897178bbfb7cbf5b5640acbee4439d1a -size 40396 +oid sha256:0e42383bf5dda6a4511cb915c40f76fee54670158c848d4ee1b125c57eea290b +size 40166 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_9_en.png index 32d51c2a7c..7a8af23c30 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9cc1a6ed77d4bdca2154e6a98294a970071f48bbd986bfb175a5ed0f0360d3a2 -size 31534 +oid sha256:a599d61446db401ad156139758917b54f702d765d24f20fce246674e6dd05cc3 +size 31555 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_4_en.png index 85af305006..8382812e13 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc7264ee36a36ac79f9c52ab24f2cdccecf149aee3b36f1f03121b5a98ac7aa8 -size 45556 +oid sha256:fddc479ceef787049b0e4af3ce61f6efbf67f7dccd81c3d6c17fcc7cea9c1127 +size 45383 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_5_en.png index ff0e6ef380..a37c7355fd 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:adbb4b7a7297bf7e586d663dc665cedc2f7dba0e841a3f93d09d3be27e3527fd -size 45919 +oid sha256:16bcb5d1d66aa3b5bb8e574e67f47e87c4beee9cc8795c4ac24ea68c57d32dbb +size 45750 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_6_en.png index c98ce28904..07b3b6b822 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d1f6072fb62c959e9685f915c3985c58b9bcc2c630d6bc9a4370f4b2d88a374c -size 39645 +oid sha256:8f99c623fc13c336ee3138741275a4fd42677d92d8357e37d6b9ae4cb75b22e1 +size 39450 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_9_en.png index 5326db7e73..b0e8833345 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f92d9cf0b0ea928325a8e64ffe32bd0484f9cb4130cd9815a2ea6997e2d5852c -size 30675 +oid sha256:093b5ba8be120cf9f93cd3448aac328c1a8ba191d45e48f514f12879e9d3d5ae +size 30702 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_FlowStepPage_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_FlowStepPage_Day_0_en.png index 71ad38ec42..ca528f64d8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_FlowStepPage_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_FlowStepPage_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe7673f9923f190509d7beaac799dab5f40a18aa6272efb15b568a1f61384d69 -size 15553 +oid sha256:57cbf6294c2eed21f53d79adccceb2cc066c391a5ab1df76e39a1a5336318789 +size 15604 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_FlowStepPage_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_FlowStepPage_Night_0_en.png index 97bc5e0f6c..27411ac3b8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_FlowStepPage_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.pages_FlowStepPage_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f38933d81b950a4e38fb8bb137cc9d7c16d23f774d2994c70e1e5899facbd19d -size 15128 +oid sha256:d28135979475db962fd91c63ed2a7f6d2cf24232b14755f961812a55c0ec4587 +size 15207 From b398fb47d2bc826207840f664b1b66197f49e72a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 2 Jun 2025 16:11:30 +0200 Subject: [PATCH 41/50] Fix mobile link (#4805) * Fix path of mobile link. Adding a trailing `/` * Reduce brain pressure. --- app/src/main/AndroidManifest.xml | 4 ++-- .../features/login/impl/DefaultLoginIntentResolver.kt | 2 +- .../login/impl/DefaultLoginIntentResolverTest.kt | 10 +++++----- tools/adb/deeplink_mobile.sh | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6619603f93..6293a9fe57 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -91,7 +91,7 @@ @@ -102,7 +102,7 @@ - +