diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index aed90d3e9f..00d8e3f765 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..2e035b6299 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 @@ -10,11 +10,13 @@ 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.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..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 @@ -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, target -> + when (action) { + is ModerationAction.DisplayProfile -> onUserDataClick(target.userId) + else -> state.roomMemberModerationState.eventSink(RoomMemberModerationEvents.ProcessAction(action, target)) + } + }, + 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..c366a1e4cf 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,6 +37,8 @@ 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 @@ -116,6 +118,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 +146,19 @@ 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..ee76562b2c 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,11 @@ 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 +298,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..a5dae403eb 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 @@ -48,7 +48,7 @@ import io.element.android.libraries.designsystem.theme.components.CircularProgre 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 +59,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 +115,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 +171,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..b3b8331a16 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 @@ -69,8 +69,8 @@ import io.element.android.libraries.designsystem.theme.components.FloatingAction import io.element.android.libraries.designsystem.theme.components.Icon 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 +92,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..1c9e8d5c2f 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,9 @@ 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.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 +125,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 +163,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..45ff052ee6 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 @@ -34,7 +34,7 @@ import io.element.android.features.messages.impl.timeline.protection.aTimelinePr 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 +48,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 +117,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..771940da0b 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 @@ -42,7 +42,7 @@ import io.element.android.libraries.designsystem.text.toPx 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 +57,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/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 3cc00d0759..070ff442f9 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 @@ -37,6 +37,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 @@ -1188,6 +1189,9 @@ class MessagesPresenterTest { textEditorState = aTextEditorStateMarkdown(initialText = "", initialFocus = false) ) }, + roomMemberModerationPresenter: Presenter = Presenter { + aRoomMemberModerationState() + }, encryptionService: FakeEncryptionService = FakeEncryptionService(), actionListEventSink: (ActionListEvents) -> Unit = {}, ): MessagesPresenter { @@ -1205,6 +1209,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..9d378f7de7 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 @@ -64,7 +67,6 @@ import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParamsAndResul import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce -import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.pressBack import kotlinx.collections.immutable.persistentListOf import org.junit.Rule @@ -310,40 +312,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..4d66365a58 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 @@ -21,7 +21,7 @@ import io.element.android.features.messages.impl.actionlist.anActionListState 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 +99,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..110545dcae 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 @@ -24,9 +24,9 @@ import io.element.android.features.messages.impl.timeline.protection.TimelinePro import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState import io.element.android.libraries.matrix.api.core.EventId 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 +175,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/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..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 @@ -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 @@ -26,8 +29,9 @@ 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 { interface Callback : Plugin { fun openRoomMemberDetails(roomMemberId: UserId) @@ -35,7 +39,6 @@ class RoomMemberListNode @AssistedInject constructor( } private val callbacks = plugins() - private val presenter = presenterFactory.create(this) init { lifecycle.subscribe( @@ -69,6 +72,16 @@ class RoomMemberListNode @AssistedInject constructor( modifier = modifier, navigator = this, ) + roomMemberModerationRenderer.Render( + state = state.moderationState, + onSelectAction = { action, target -> + when (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 e2527444aa..ec63f383d3 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,11 +16,9 @@ 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.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 @@ -33,6 +31,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.roomMemberIdentityStateChange import kotlinx.collections.immutable.ImmutableMap @@ -43,20 +42,15 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.withContext +import javax.inject.Inject -class RoomMemberListPresenter @AssistedInject constructor( +class RoomMemberListPresenter @Inject 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 { - @AssistedFactory - interface Factory { - fun create(navigator: RoomMemberListNavigator): RoomMemberListPresenter - } - @Composable override fun present(): RoomMemberListState { var roomMembers: AsyncData by remember { mutableStateOf(AsyncData.Loading()) } @@ -69,7 +63,6 @@ class RoomMemberListPresenter @AssistedInject constructor( val membersState by room.membersStateFlow.collectAsState() val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val canInvite by room.canInviteAsState(syncUpdateFlow.value) - val roomModerationState = roomMembersModerationPresenter.present() val roomMemberIdentityStates by produceState(persistentMapOf()) { @@ -163,10 +156,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.toMatrixUser())) } else { - navigator.openRoomMemberDetails(event.roomMember.userId) + roomModerationState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(event.roomMember.toMatrixUser())) } } } 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..c5d2a93e2a 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,6 @@ 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.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.button.BackButton @@ -99,7 +98,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 +126,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 +134,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/RoomMembersModerationPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenter.kt deleted file mode 100644 index af71e21dff..0000000000 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationPresenter.kt +++ /dev/null @@ -1,187 +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.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.libraries.architecture.AsyncAction -import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.architecture.runUpdatingState -import io.element.android.libraries.core.coroutine.CoroutineDispatchers -import io.element.android.libraries.matrix.api.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.toPersistentList -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.drop -import kotlinx.coroutines.flow.take -import kotlinx.coroutines.launch -import javax.inject.Inject - -class RoomMembersModerationPresenter @Inject constructor( - private val room: JoinedRoom, - private val dispatchers: CoroutineDispatchers, - private val analyticsService: AnalyticsService, -) : Presenter { - private var selectedMember by mutableStateOf(null) - - @Composable - override fun present(): RoomMembersModerationState { - 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 kickUserAsyncAction = - remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction) } - val banUserAsyncAction = - remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction) } - val unbanUserAsyncAction = - remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction) } - - fun handleEvent(event: RoomMembersModerationEvents) { - 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 RoomMembersModerationEvents.KickUser -> { - kickUserAsyncAction.value = AsyncAction.ConfirmingNoParams - } - is RoomMembersModerationEvents.DoKickUser -> { - selectedMember?.let { - coroutineScope.kickUser(it.userId, event.reason, kickUserAsyncAction) - } - selectedMember = null - } - is RoomMembersModerationEvents.BanUser -> { - banUserAsyncAction.value = AsyncAction.ConfirmingNoParams - } - is RoomMembersModerationEvents.DoBanUser -> { - selectedMember?.let { - coroutineScope.banUser(it.userId, event.reason, banUserAsyncAction) - } - selectedMember = null - } - is RoomMembersModerationEvents.UnbanUser -> { - // We are already confirming when we are reaching this point - coroutineScope.unbanUser(event.userId, unbanUserAsyncAction) - } - is RoomMembersModerationEvents.Reset -> { - selectedMember = null - kickUserAsyncAction.value = AsyncAction.Uninitialized - banUserAsyncAction.value = AsyncAction.Uninitialized - unbanUserAsyncAction.value = AsyncAction.Uninitialized - } - } - } - - return RoomMembersModerationState( - canDisplayModerationActions = canDisplayModerationActions, - selectedRoomMember = selectedMember, - actions = moderationActions, - kickUserAsyncAction = kickUserAsyncAction.value, - banUserAsyncAction = banUserAsyncAction.value, - unbanUserAsyncAction = unbanUserAsyncAction.value, - canDisplayBannedUsers = canDisplayBannedUsers, - eventSink = { handleEvent(it) }, - ) - } - - private fun CoroutineScope.kickUser( - userId: UserId, - reason: String, - kickUserAction: MutableState>, - ) = runActionAndWaitForMembershipChange(kickUserAction) { - analyticsService.capture(RoomModeration(RoomModeration.Action.KickMember)) - room.kickUser( - userId = userId, - reason = reason.takeIf { it.isNotBlank() }, - ) - } - - private fun CoroutineScope.banUser( - userId: UserId, - reason: String, - banUserAction: MutableState>, - ) = runActionAndWaitForMembershipChange(banUserAction) { - analyticsService.capture(RoomModeration(RoomModeration.Action.BanMember)) - room.banUser( - userId = userId, - reason = reason.takeIf { it.isNotBlank() }, - ) - } - - private fun CoroutineScope.unbanUser( - userId: UserId, - unbanUserAction: MutableState>, - ) = runActionAndWaitForMembershipChange(unbanUserAction) { - analyticsService.capture(RoomModeration(RoomModeration.Action.UnbanMember)) - room.unbanUser(userId) - } - - private fun CoroutineScope.runActionAndWaitForMembershipChange( - action: MutableState>, - block: suspend () -> Result - ) { - launch(dispatchers.io) { - action.runUpdatingState { - val result = block() - if (result.isSuccess) { - room.membersStateFlow.drop(1).take(1) - } - result - } - } - } -} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/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/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..f2436179f7 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,12 +11,10 @@ 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 import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.RoomMembersState @@ -24,7 +22,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 +39,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 +94,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 +201,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,47 +219,8 @@ 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())) - } - } -} - -private class FakeRoomMemberListNavigator : RoomMemberListNavigator { - var openRoomMemberDetailsCallCount = 0 - private set - - override fun openRoomMemberDetails(roomMemberId: UserId) { - openRoomMemberDetailsCallCount++ - } } @ExperimentalCoroutinesApi @@ -277,19 +235,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/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..da25ca41eb --- /dev/null +++ b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationEvents.kt @@ -0,0 +1,15 @@ +/* + * 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 io.element.android.libraries.matrix.api.user.MatrixUser + +interface RoomMemberModerationEvents { + data class ShowActionsForUser(val user: MatrixUser) : 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 new file mode 100644 index 0000000000..afe8ab0f8b --- /dev/null +++ b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationRenderer.kt @@ -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. + */ + +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, 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 new file mode 100644 index 0000000000..368aa283ad --- /dev/null +++ b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationState.kt @@ -0,0 +1,26 @@ +/* + * 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 + +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 object DisplayProfile : ModerationAction + data object KickUser : ModerationAction + data object BanUser : ModerationAction + data object UnbanUser : ModerationAction +} diff --git a/features/roommembermoderation/impl/build.gradle.kts b/features/roommembermoderation/impl/build.gradle.kts new file mode 100644 index 0000000000..93294988f6 --- /dev/null +++ b/features/roommembermoderation/impl/build.gradle.kts @@ -0,0 +1,47 @@ +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" + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } +} + +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.molecule.runtime) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) + testImplementation(projects.libraries.matrix.test) + testImplementation(projects.tests.testutils) + testImplementation(projects.services.analytics.test) + testImplementation(libs.test.robolectric) + testImplementation(libs.androidx.compose.ui.test.junit) + testReleaseImplementation(libs.androidx.compose.ui.test.manifest) + testImplementation(projects.libraries.testtags) +} 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..681a1eb733 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/DefaultRoomMemberModerationRenderer.kt @@ -0,0 +1,38 @@ +/* + * 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 io.element.android.libraries.matrix.api.user.MatrixUser +import timber.log.Timber +import javax.inject.Inject + +@ContributesBinding(RoomScope::class) +class DefaultRoomMemberModerationRenderer @Inject constructor() : RoomMemberModerationRenderer { + @Composable + override fun Render( + state: RoomMemberModerationState, + onSelectAction: (ModerationAction, MatrixUser) -> 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..902c2bd21f --- /dev/null +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationEvents.kt @@ -0,0 +1,17 @@ +/* + * 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 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..3a650ee59c --- /dev/null +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationState.kt @@ -0,0 +1,28 @@ +/* + * 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 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.matrix.api.user.MatrixUser +import kotlinx.collections.immutable.ImmutableList + +data class InternalRoomMemberModerationState( + override val canKick: Boolean, + override val canBan: Boolean, + val selectedUser: MatrixUser?, + val actions: ImmutableList, + val kickUserAsyncAction: AsyncAction, + val banUserAsyncAction: AsyncAction, + val unbanUserAsyncAction: AsyncAction, + override val eventSink: (RoomMemberModerationEvents) -> Unit, +) : RoomMemberModerationState { + val canDisplayActions = actions.isNotEmpty() +} diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationStateProvider.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationStateProvider.kt new file mode 100644 index 0000000000..bdebcc3a93 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationStateProvider.kt @@ -0,0 +1,94 @@ +/* + * 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.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.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.user.MatrixUser +import kotlinx.collections.immutable.toPersistentList + +class InternalRoomMemberModerationStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aRoomMembersModerationState( + selectedUser = anAlice(), + actions = listOf( + ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true), + ), + ), + aRoomMembersModerationState( + selectedUser = anAlice(), + actions = listOf( + ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true), + ModerationActionState(action = ModerationAction.KickUser, isEnabled = true), + ), + ), + aRoomMembersModerationState( + selectedUser = anAlice(), + actions = listOf( + ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true), + ModerationActionState(action = ModerationAction.KickUser, isEnabled = false), + ModerationActionState(action = ModerationAction.BanUser, isEnabled = true), + ), + ), + aRoomMembersModerationState( + selectedUser = anAlice(), + actions = listOf( + ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true), + ModerationActionState(action = ModerationAction.KickUser, isEnabled = false), + ModerationActionState(action = ModerationAction.UnbanUser, isEnabled = true), + ), + ), + aRoomMembersModerationState( + selectedUser = anAlice(), + kickUserAsyncAction = AsyncAction.ConfirmingNoParams, + ), + aRoomMembersModerationState( + selectedUser = anAlice(), + kickUserAsyncAction = AsyncAction.Loading, + ), + aRoomMembersModerationState( + selectedUser = anAlice(), + banUserAsyncAction = AsyncAction.ConfirmingNoParams, + ), + aRoomMembersModerationState( + selectedUser = anAlice(), + banUserAsyncAction = AsyncAction.Loading, + ), + ) +} + +fun anAlice() = MatrixUser( + UserId(value = "@alice:server.org"), + displayName = "Alice", + avatarUrl = null, +) + +fun aRoomMembersModerationState( + canKick: Boolean = false, + canBan: Boolean = false, + selectedUser: MatrixUser? = null, + actions: List = emptyList(), + kickUserAsyncAction: AsyncAction = AsyncAction.Uninitialized, + banUserAsyncAction: AsyncAction = AsyncAction.Uninitialized, + unbanUserAsyncAction: AsyncAction = AsyncAction.Uninitialized, + eventSink: (RoomMemberModerationEvents) -> Unit = {}, +) = InternalRoomMemberModerationState( + canKick = canKick, + canBan = canBan, + selectedUser = selectedUser, + actions = actions.toPersistentList(), + kickUserAsyncAction = kickUserAsyncAction, + banUserAsyncAction = banUserAsyncAction, + unbanUserAsyncAction = unbanUserAsyncAction, + eventSink = eventSink, +) 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 new file mode 100644 index 0000000000..f0861a735e --- /dev/null +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt @@ -0,0 +1,213 @@ +/* + * 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.MutableState +import androidx.compose.runtime.collectAsState +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.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.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.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 +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 +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.launch +import javax.inject.Inject + +class RoomMemberModerationPresenter @Inject constructor( + private val room: JoinedRoom, + private val dispatchers: CoroutineDispatchers, + private val analyticsService: AnalyticsService, +) : Presenter { + @Composable + override fun present(): RoomMemberModerationState { + val coroutineScope = rememberCoroutineScope() + val syncUpdateFlow = room.syncUpdateFlow.collectAsState() + 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) } + val banUserAsyncAction = + 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.ShowActionsForUser -> { + selectedUser = event.user + 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 (event.action) { + is ModerationAction.DisplayProfile -> Unit + is ModerationAction.KickUser -> { + selectedUser = event.targetUser + kickUserAsyncAction.value = AsyncAction.ConfirmingNoParams + } + is ModerationAction.BanUser -> { + selectedUser = event.targetUser + banUserAsyncAction.value = AsyncAction.ConfirmingNoParams + } + is ModerationAction.UnbanUser -> { + selectedUser = event.targetUser + unbanUserAsyncAction.value = AsyncAction.ConfirmingNoParams + } + } + } + is InternalRoomMemberModerationEvents.DoKickUser -> { + selectedUser?.let { + coroutineScope.kickUser(it.userId, event.reason, kickUserAsyncAction) + } + selectedUser = null + } + is InternalRoomMemberModerationEvents.DoBanUser -> { + selectedUser?.let { + coroutineScope.banUser(it.userId, event.reason, banUserAsyncAction) + } + selectedUser = null + } + 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 + } + } + } + + return InternalRoomMemberModerationState( + canKick = canKick.value, + canBan = canBan.value, + selectedUser = selectedUser, + actions = moderationActions.value, + kickUserAsyncAction = kickUserAsyncAction.value, + banUserAsyncAction = banUserAsyncAction.value, + unbanUserAsyncAction = unbanUserAsyncAction.value, + eventSink = { handleEvent(it) }, + ) + } + + private fun computeModerationActions( + member: RoomMember?, + canKick: Boolean, + canBan: Boolean, + currentUserMemberPowerLevel: Long, + ): PersistentList { + return buildList { + add(ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true)) + // Assume the member is a regular user when it's unknown + val targetMemberPowerLevel = member?.powerLevel ?: 0 + val canModerateThisUser = currentUserMemberPowerLevel > targetMemberPowerLevel + // 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) { + if (membership == RoomMembershipState.BAN) { + add(ModerationActionState(action = ModerationAction.UnbanUser, isEnabled = canModerateThisUser)) + } else { + add(ModerationActionState(action = ModerationAction.BanUser, isEnabled = canModerateThisUser)) + } + } + }.toPersistentList() + } + + private fun CoroutineScope.kickUser( + userId: UserId, + reason: String, + kickUserAction: MutableState>, + ) = runActionAndWaitForMembershipChange(kickUserAction) { + analyticsService.capture(RoomModeration(RoomModeration.Action.KickMember)) + room.kickUser( + userId = userId, + reason = reason.takeIf { it.isNotBlank() }, + ) + } + + private fun CoroutineScope.banUser( + userId: UserId, + reason: String, + banUserAction: MutableState>, + ) = runActionAndWaitForMembershipChange(banUserAction) { + analyticsService.capture(RoomModeration(RoomModeration.Action.BanMember)) + room.banUser( + userId = userId, + reason = reason.takeIf { it.isNotBlank() }, + ) + } + + private fun CoroutineScope.unbanUser( + userId: UserId, + unbanUserAction: MutableState>, + ) = runActionAndWaitForMembershipChange(unbanUserAction) { + analyticsService.capture(RoomModeration(RoomModeration.Action.UnbanMember)) + room.unbanUser(userId = userId) + } + + private fun CoroutineScope.runActionAndWaitForMembershipChange( + action: MutableState>, + block: suspend () -> Result + ) { + launch(dispatchers.io) { + action.runUpdatingState { + val result = block() + if (result.isSuccess) { + room.membersStateFlow.drop(1).take(1) + } + result + } + } + } +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationView.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt similarity index 61% 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..5a99607729 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 @@ -1,11 +1,11 @@ /* - * Copyright 2024 New Vector Ltd. + * 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.roomdetails.impl.members.moderation +package io.element.android.features.roommembermoderation.impl import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -30,7 +30,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons -import io.element.android.features.roomdetails.impl.R +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 @@ -47,66 +48,64 @@ 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.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 import timber.log.Timber @Composable -fun RoomMembersModerationView( - state: RoomMembersModerationState, - onDisplayMemberProfile: (UserId) -> Unit, +fun RoomMemberModerationView( + state: InternalRoomMemberModerationState, + onSelectAction: (ModerationAction, MatrixUser) -> Unit, modifier: Modifier = Modifier, ) { Box(modifier = modifier) { - if (state.selectedRoomMember != null && state.actions.isNotEmpty()) { + val selectedUser = state.selectedUser + if (selectedUser != null && state.canDisplayActions) { RoomMemberActionsBottomSheet( - roomMember = state.selectedRoomMember, + user = selectedUser, 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) }, ) } + 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) 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 = selectedUser?.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 +128,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 = selectedUser?.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 +163,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_bottom_sheet_manage_room_member_unban_member_confirmation_title), + content = stringResource(R.string.screen_bottom_sheet_manage_room_member_unban_member_confirmation_description), + submitText = stringResource(R.string.screen_bottom_sheet_manage_room_member_unban_member_confirmation_action), + onSubmitClick = { + val userDisplayName = selectedUser?.getBestName().orEmpty() + asyncIndicatorState.enqueue { + AsyncIndicator.Loading(text = stringResource(R.string.screen_bottom_sheet_manage_room_member_unbanning_user, userDisplayName)) + } + state.eventSink(InternalRoomMemberModerationEvents.DoUnbanUser) + }, + onDismiss = { state.eventSink(InternalRoomMemberModerationEvents.Reset) }, + ) } is AsyncAction.Failure -> { Timber.e(action.error, "Failed to unban user.") @@ -205,9 +201,9 @@ fun RoomMembersModerationView( @OptIn(ExperimentalMaterial3Api::class) @Composable private fun RoomMemberActionsBottomSheet( - roomMember: RoomMember, - actions: ImmutableList, - onSelectAction: (ModerationAction) -> Unit, + user: MatrixUser, + actions: ImmutableList, + onSelectAction: (ModerationAction, MatrixUser) -> Unit, onDismiss: () -> Unit, ) { val coroutineScope = rememberCoroutineScope() @@ -226,12 +222,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) + .padding(bottom = 28.dp) + .align(Alignment.CenterHorizontally) ) - roomMember.displayName?.let { + user.displayName?.let { Text( text = it, style = ElementTheme.typography.fontHeadingLgBold, @@ -239,60 +235,78 @@ 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( - text = roomMember.userId.toString(), + text = user.userId.toString(), style = ElementTheme.typography.fontBodyLgRegular, color = ElementTheme.colors.textSecondary, maxLines = 1, overflow = TextOverflow.Ellipsis, textAlign = TextAlign.Center, modifier = Modifier - .padding(horizontal = 16.dp) - .fillMaxWidth() + .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(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 { - onSelectAction(action) + onSelectAction(action, user) bottomSheetState.hide() } - } + }, + enabled = actionState.isEnabled ) } is ModerationAction.KickUser -> { ListItem( - headlineContent = { Text(stringResource(CommonStrings.screen_bottom_sheet_manage_room_member_remove)) }, - leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Block())), + headlineContent = { Text(stringResource(R.string.screen_bottom_sheet_manage_room_member_remove)) }, + 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 -> { 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 = { coroutineScope.launch { bottomSheetState.hide() - onSelectAction(action) + onSelectAction(action, user) } - } + }, + enabled = actionState.isEnabled + ) + } + is ModerationAction.UnbanUser -> { + ListItem( + headlineContent = { Text(stringResource(R.string.screen_bottom_sheet_manage_room_member_unban)) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Restart())), + style = ListItemStyle.Destructive, + onClick = { + coroutineScope.launch { + bottomSheetState.hide() + onSelectAction(action, user) + } + }, + enabled = actionState.isEnabled ) } } @@ -303,16 +317,17 @@ private fun RoomMemberActionsBottomSheet( @PreviewsDayNight @Composable -internal fun RoomMembersModerationViewPreview(@PreviewParameter(RoomMembersModerationStateProvider::class) state: RoomMembersModerationState) { +internal fun RoomMemberModerationViewPreview(@PreviewParameter(InternalRoomMemberModerationStateProvider::class) state: InternalRoomMemberModerationState) { ElementPreview { Box( modifier = Modifier - .fillMaxWidth() - .heightIn(min = 64.dp) + .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..d2a5296b95 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/di/RoomMemberModerationModule.kt @@ -0,0 +1,23 @@ +/* + * 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 + +@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..bd744e96e7 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-be/translations.xml @@ -0,0 +1,12 @@ + + + "Выдаліць і заблакіраваць удзельніка" + "Заблакіраваць" + "Яны не змогуць зноў далучыцца да гэтага пакоя, калі іх запросяць." + "Вы ўпэўнены, што хочаце заблакіраваць гэтага карыстальніка?" + "Блакіроўка %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..2c5e51972e --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-cs/translations.xml @@ -0,0 +1,20 @@ + + + "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…" + "Zrušit vykázání z místnosti" + "Zrušit vykázání" + "Pokud by byli pozváni, mohli by se znovu připojit do místnosti" + "Opravdu chcete zrušit vykázání tohoto člena?" + "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..cfa26c6e9b --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-cy/translations.xml @@ -0,0 +1,20 @@ + + + "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…" + "Dad-wahardd o\'r ystafell" + "Dad-wahardd" + "Bydden nhw\'n gallu ymuno â\'r ystafell eto os fydd rhywun yn eu gwahodd" + "Ydych chi\'n siŵr eich bod chi eisiau dadwahardd yr aelod hwn?" + "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..3ded15cd8e --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,20 @@ + + + "Mitglied entfernen und sperren" + "Sperren" + "Sie können dem Raum nicht mehr beitreten, selbst wenn sie eingeladen werden." + "Möchten Sie diesen Nutzer wirklich sperren?" + "%1$s wird gesperrt." + "Entfernen" + "Sie können diesen Raum wieder betreten, wenn sie eingeladen werden." + "Möchten Sie dieses Mitglied wirklich entfernen?" + "Nutzerprofil anzeigen" + "Mitglied entfernen" + "Mitglied entfernen und für die Zukunft sperren?" + "%1$s wird entfernt." + "Sperre für diesen Chatroom aufheben" + "Sperre aufheben" + "Sie könnten den Chatroom wieder betreten, wenn sie wieder eingeladen würden." + "Möchten Sie die Sperre dieses Mitglieds wirklich aufheben?" + "Sperre für %1$s aufheben" + 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..5c81a56396 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-el/translations.xml @@ -0,0 +1,15 @@ + + + "Αφαίρεση και αποκλεισμός μέλους" + "Αποκλεισμός" + "Δεν θα μπορεί να συμμετέχει ξανά σε αυτό το δωμάτιο εάν προσκληθεί." + "Θες σίγουρα να αποκλείσεις αυτό το μέλος;" + "Αποκλεισμός %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..04049e9e31 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-es/translations.xml @@ -0,0 +1,20 @@ + + + "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" + "Echar" + "Podrá volver a unirse a esta sala si se le invita." + "¿Seguro que quieres echar a este miembro?" + "Ver perfil" + "Sacar de la sala" + "¿Sacar al miembro y prohibirle unirse en el futuro?" + "Eliminando %1$s…" + "Eliminar veto en la sala" + "Eliminar veto" + "Podría volver a unirse a la sala si se le invita" + "¿Seguro que quieres levantarle el veto a este miembro?" + "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..ffb1d79f88 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-et/translations.xml @@ -0,0 +1,20 @@ + + + "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 suhtluskeeld jututoas" + "Eemalda suhtluskeeld" + "Ta võib kutse saamisel liituda jututoaga uuesti" + "Kas oled kindel, et soovid selle liikme suhtluskeelu eemaldada?" + "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..c49ae2d4d5 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-eu/translations.xml @@ -0,0 +1,11 @@ + + + "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…" + 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..a51b49b232 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-fa/translations.xml @@ -0,0 +1,15 @@ + + + "برداشت و تحریم عضو" + "تحریم" + "در صورت دعوت نمی‌تواند دوباره به اتاق بپیوندد." + "مطمئنید می‌خواهید این عضو را تحریم کنید؟" + "تحریم کردن %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..4c93f73780 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-fi/translations.xml @@ -0,0 +1,20 @@ + + + "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 porttikielto huoneesta" + "Poista porttikielto" + "He voivat liittyä huoneeseen uudelleen, jos heidät kutsutaan" + "Haluatko varmasti poistaa tämän jäsenen porttikiellon?" + "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..9aabe07e11 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-fr/translations.xml @@ -0,0 +1,20 @@ + + + "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…" + "Débannir du salon" + "Débannir" + "L’utilisateur pourra à nouveau rejoindre le salon s’il est invité." + "Êtes-vous sûr de vouloir débannir cet 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..db36f41964 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-hu/translations.xml @@ -0,0 +1,20 @@ + + + "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…" + "Visszaengedés a szobába" + "Kitiltás visszavonása" + "Újra beléphetnek a szobába, ha meghívják őket." + "Biztos, hogy feloldja a felhasználó kitiltását?" + "%1$s kitiltásának feloldása" + diff --git a/features/roommembermoderation/impl/src/main/res/values-in/translations.xml b/features/roommembermoderation/impl/src/main/res/values-in/translations.xml new file mode 100644 index 0000000000..c40e811d9c --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-in/translations.xml @@ -0,0 +1,12 @@ + + + "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…" + 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..e4f2508747 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-it/translations.xml @@ -0,0 +1,12 @@ + + + "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…" + 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..a2eb6d4bad --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-ka/translations.xml @@ -0,0 +1,12 @@ + + + "წევრის წაშლა და დაბლოკვა" + "დაბლოკვა" + "მოწვევის შემთხვევაში ამ ოთახში კვლავ გაწევრიანებას ვერ შეძლებენ." + "დარწმუნებული ხართ, რომ ამ წევრის დაბლოკვა გსურთ?" + "%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..323526170b --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-nb/translations.xml @@ -0,0 +1,20 @@ + + + "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 utestengelsen fra rommet" + "Opphev utestengelsen" + "De vil kunne bli med i rommet igjen hvis de blir invitert" + "Er du sikker på at du vil oppheve utestengelsen av dette medlemmet?" + "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..64317b83bf --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-nl/translations.xml @@ -0,0 +1,12 @@ + + + "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…" + 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..4b0098c34a --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-pl/translations.xml @@ -0,0 +1,15 @@ + + + "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…" + 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..036c87f2b7 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-pt-rBR/translations.xml @@ -0,0 +1,15 @@ + + + "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" + "Remover" + "Eles poderão entrar nesta sala novamente se forem convidados." + "Tem certeza de que deseja remover este membro?" + "Ver perfil" + "Remover da sala" + "Remover membro e banir de entrar novamente no futuro?" + "Removendo %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..5d063c8ad2 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-pt/translations.xml @@ -0,0 +1,20 @@ + + + "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" + "Remover" + "Poderão entrar na sala novamente se convidados." + "Tens a certeza que queres remover este membro?" + "Ver perfil" + "Remover da sala" + "Remover participante e proibir que entre no futuro?" + "A remover %1$s…" + "Anular banimento da sala" + "Anular banimento" + "Poderão entrar novamente na sala se forem convidados" + "Tens a certeza que queres anular o banimento deste 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..27bb9c23bd --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-ro/translations.xml @@ -0,0 +1,12 @@ + + + "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" + 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..ca4610dd31 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-ru/translations.xml @@ -0,0 +1,14 @@ + + + "Удалить и заблокировать участника" + "Заблокировать" + "Они не смогут снова присоединиться к этой комнате, если их пригласят." + "Вы уверены, что хотите заблокировать этого участника?" + "Блокировка %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..df5214c17c --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-sk/translations.xml @@ -0,0 +1,20 @@ + + + "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…" + "Zrušiť zákaz prístupu do miestnosti" + "Zrušiť zákaz" + "V prípade pozvania by sa mohli opäť pripojiť k miestnosti" + "Naozaj chcete zrušiť zablokovanie tohto člena?" + "Zrušenie zákazu pre %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..9f3aabe191 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-sv/translations.xml @@ -0,0 +1,20 @@ + + + "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 …" + "Avbanna från rummet" + "Avbanna" + "De skulle kunna gå med i rummet igen om de blev inbjudna" + "Är du säker på att du vill avbanna den här medlemmen?" + "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..73f17edd02 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-tr/translations.xml @@ -0,0 +1,12 @@ + + + "Ü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…" + 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..6c48b0bde0 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-uk/translations.xml @@ -0,0 +1,20 @@ + + + "Вилучити й заблокувати учасника" + "Заблокувати" + "Він не зможе приєднатися до цієї кімнати знову, якщо його запросять." + "Ви точно хочете заблокувати цього користувача?" + "Блокування %1$s" + "Вилучити" + "Вони зможуть знову приєднатися до цієї кімнати, якщо їх запросять." + "Ви дійсно хочете вилучити цього учасника?" + "Переглянути профіль" + "Вилучити з кімнати" + "Вилучити учасника та заборонити приєднання в майбутньому?" + "Вилучення %1$s…" + "Розблокувати в кімнаті" + "Розблокувати" + "Вони зможуть знову приєднатися до кімнати, якщо їх запросять" + "Ви впевнені, що хочете розблокувати цього учасника?" + "Розблокування %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-ur/translations.xml b/features/roommembermoderation/impl/src/main/res/values-ur/translations.xml new file mode 100644 index 0000000000..333c86f056 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-ur/translations.xml @@ -0,0 +1,12 @@ + + + "کمرے سے محظور کریں" + "محظور کریں" + "اگر وہ مدعو کیا گیا تو وہ دوبارہ اس کمرے میں شامل نہیں ہوسکیں گے۔" + "کیا آپ کو یقین ہے کہ آپ اس رکن کو محظور کرنا چاہتے ہیں؟" + "%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..152e2d3a8f --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-zh-rTW/translations.xml @@ -0,0 +1,15 @@ + + + "踢出並加入黑名單" + "加入黑名單" + "即使收到邀請,他們仍然無法加入聊天室。" + "您確定要將此成員加入黑名單?" + "正在將 %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..20c002aa76 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-zh/translations.xml @@ -0,0 +1,15 @@ + + + "移除并封禁成员" + "封禁" + "即使受到邀请,他们也无法再次加入聊天室。" + "您确定要封禁该成员吗?" + "封禁 %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..16b013d537 --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values/localazy.xml @@ -0,0 +1,20 @@ + + + "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…" + "Unban from room" + "Unban" + "They would be able to join the room again if invited" + "Are you sure you want to unban this member?" + "Unbanning %1$s" + diff --git a/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenterTest.kt b/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenterTest.kt new file mode 100644 index 0000000000..3496612c35 --- /dev/null +++ b/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenterTest.kt @@ -0,0 +1,360 @@ +/* + * 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 app.cash.turbine.TurbineTestContext +import com.google.common.truth.Truth.assertThat +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.core.coroutine.CoroutineDispatchers +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.RoomMembersState +import io.element.android.libraries.matrix.api.room.RoomMembershipState +import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.test.A_USER_ID +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.aRoomMember +import io.element.android.services.analytics.api.AnalyticsService +import io.element.android.services.analytics.test.FakeAnalyticsService +import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.test +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class RoomMemberModerationPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + private val targetUser = MatrixUser(userId = A_USER_ID) + + @Test + fun `present - initial state`() = runTest { + val room = aJoinedRoom() + createRoomMemberModerationPresenter(room = room).test { + val initialState = awaitState() + assertThat(initialState.canKick).isFalse() + assertThat(initialState.canBan).isFalse() + assertThat(initialState.selectedUser).isNull() + assertThat(initialState.banUserAsyncAction).isEqualTo(AsyncAction.Uninitialized) + assertThat(initialState.kickUserAsyncAction).isEqualTo(AsyncAction.Uninitialized) + assertThat(initialState.unbanUserAsyncAction).isEqualTo(AsyncAction.Uninitialized) + assertThat(initialState.actions).isEmpty() + } + } + + @Test + fun `present - show actions when canBan=false, canKick=false`() = runTest { + val room = aJoinedRoom( + canBan = false, + canKick = false, + myUserRole = RoomMember.Role.USER, + targetRoomMember = aRoomMember(userId = A_USER_ID, powerLevel = RoomMember.Role.USER.powerLevel) + ) + createRoomMemberModerationPresenter(room = room).test { + val initialState = awaitState() + initialState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(targetUser)) + skipItems(1) + val updatedState = awaitState() + assertThat(updatedState.selectedUser).isEqualTo(targetUser) + assertThat(updatedState.actions).containsExactly( + ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true), + ) + } + } + + @Test + fun `present - show actions when canBan=true, canKick=true, userRole=Admin and target member is unknown`() = runTest { + val room = aJoinedRoom( + canBan = true, + canKick = true, + myUserRole = RoomMember.Role.ADMIN, + targetRoomMember = null + ) + createRoomMemberModerationPresenter(room = room).test { + val initialState = awaitState() + initialState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(targetUser)) + skipItems(2) + val updatedState = awaitState() + assertThat(updatedState.selectedUser).isEqualTo(targetUser) + assertThat(updatedState.actions).containsExactly( + ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true), + ModerationActionState(action = ModerationAction.KickUser, isEnabled = true), + ModerationActionState(action = ModerationAction.BanUser, isEnabled = true), + ) + } + } + + @Test + fun `show actions when canBan=true, canKick=true, userRole=Admin and target is User`() = runTest { + val room = aJoinedRoom( + canBan = true, + canKick = true, + myUserRole = RoomMember.Role.ADMIN, + targetRoomMember = aRoomMember(userId = A_USER_ID, powerLevel = RoomMember.Role.USER.powerLevel) + ) + createRoomMemberModerationPresenter(room = room).test { + val initialState = awaitState() + initialState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(targetUser)) + skipItems(2) + val updatedState = awaitState() + assertThat(updatedState.selectedUser).isEqualTo(targetUser) + assertThat(updatedState.actions).containsExactly( + ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true), + ModerationActionState(action = ModerationAction.KickUser, isEnabled = true), + ModerationActionState(action = ModerationAction.BanUser, isEnabled = true), + ) + } + } + + @Test + fun `show actions when canBan=true, canKick=true, userRole=Moderator and target is Admin`() = runTest { + val room = aJoinedRoom( + canBan = true, + canKick = true, + myUserRole = RoomMember.Role.MODERATOR, + targetRoomMember = aRoomMember(userId = A_USER_ID, powerLevel = RoomMember.Role.ADMIN.powerLevel) + ) + createRoomMemberModerationPresenter(room = room).test { + val initialState = awaitState() + initialState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(targetUser)) + skipItems(2) + val updatedState = awaitState() + assertThat(updatedState.selectedUser).isEqualTo(targetUser) + assertThat(updatedState.actions).containsExactly( + ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true), + ModerationActionState(action = ModerationAction.KickUser, isEnabled = false), + ModerationActionState(action = ModerationAction.BanUser, isEnabled = false), + ) + } + } + + @Test + fun `show actions when canBan=true, canKick=true, userRole=Moderator and target is Banned`() = runTest { + val room = aJoinedRoom( + canBan = true, + canKick = true, + myUserRole = RoomMember.Role.MODERATOR, + targetRoomMember = aRoomMember(userId = A_USER_ID, membership = RoomMembershipState.BAN) + ) + createRoomMemberModerationPresenter(room = room).test { + val initialState = awaitState() + initialState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(targetUser)) + skipItems(2) + val updatedState = awaitState() + assertThat(updatedState.selectedUser).isEqualTo(targetUser) + assertThat(updatedState.actions).containsExactly( + ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true), + ModerationActionState(action = ModerationAction.KickUser, isEnabled = false), + ModerationActionState(action = ModerationAction.UnbanUser, isEnabled = true), + ) + } + } + + @Test + fun `present - process kick action sets confirming state`() = runTest { + createRoomMemberModerationPresenter(room = aJoinedRoom()).test { + val initialState = awaitState() + initialState.eventSink( + RoomMemberModerationEvents.ProcessAction( + targetUser = targetUser, + action = ModerationAction.KickUser + ) + ) + skipItems(1) + val updatedState = awaitState() + assertThat(updatedState.selectedUser).isEqualTo(targetUser) + assertThat(updatedState.kickUserAsyncAction).isEqualTo(AsyncAction.ConfirmingNoParams) + } + } + + @Test + fun `present - process ban action sets confirming state`() = runTest { + createRoomMemberModerationPresenter(room = aJoinedRoom()).test { + val initialState = awaitState() + initialState.eventSink( + RoomMemberModerationEvents.ProcessAction( + targetUser = targetUser, + action = ModerationAction.BanUser + ) + ) + skipItems(1) + val updatedState = awaitState() + assertThat(updatedState.selectedUser).isEqualTo(targetUser) + assertThat(updatedState.banUserAsyncAction).isEqualTo(AsyncAction.ConfirmingNoParams) + } + } + + @Test + fun `present - process unban action sets confirming state`() = runTest { + createRoomMemberModerationPresenter(room = aJoinedRoom()).test { + val initialState = awaitState() + initialState.eventSink( + RoomMemberModerationEvents.ProcessAction( + targetUser = targetUser, + action = ModerationAction.UnbanUser + ) + ) + skipItems(1) + val updatedState = awaitState() + assertThat(updatedState.selectedUser).isEqualTo(targetUser) + assertThat(updatedState.unbanUserAsyncAction).isEqualTo(AsyncAction.ConfirmingNoParams) + } + } + + @Test + fun `present - do kick user with success`() = runTest { + createRoomMemberModerationPresenter(room = aJoinedRoom()).test { + val initialState = awaitState() + initialState.eventSink( + RoomMemberModerationEvents.ProcessAction( + targetUser = targetUser, + action = ModerationAction.KickUser + ) + ) + skipItems(2) + initialState.eventSink(InternalRoomMemberModerationEvents.DoKickUser("Reason")) + skipItems(1) + val loadingState = awaitState() + assertThat(loadingState.kickUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java) + val successState = awaitState() + assertThat(successState.kickUserAsyncAction).isInstanceOf(AsyncAction.Success::class.java) + assertThat(successState.selectedUser).isNull() + } + } + + @Test + fun `present - do ban user with success`() = runTest { + createRoomMemberModerationPresenter(room = aJoinedRoom()).test { + val initialState = awaitState() + initialState.eventSink( + RoomMemberModerationEvents.ProcessAction( + targetUser = targetUser, + action = ModerationAction.BanUser + ) + ) + skipItems(2) + initialState.eventSink(InternalRoomMemberModerationEvents.DoBanUser("Reason")) + skipItems(1) + val loadingState = awaitState() + assertThat(loadingState.banUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java) + val successState = awaitState() + assertThat(successState.banUserAsyncAction).isInstanceOf(AsyncAction.Success::class.java) + assertThat(successState.selectedUser).isNull() + } + } + + @Test + fun `present - do unban user with success`() = runTest { + createRoomMemberModerationPresenter(room = aJoinedRoom()).test { + val initialState = awaitState() + initialState.eventSink( + RoomMemberModerationEvents.ProcessAction( + targetUser = targetUser, + action = ModerationAction.UnbanUser + ) + ) + skipItems(2) + initialState.eventSink(InternalRoomMemberModerationEvents.DoUnbanUser) + skipItems(1) + val loadingState = awaitState() + assertThat(loadingState.unbanUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java) + val successState = awaitState() + assertThat(successState.unbanUserAsyncAction).isInstanceOf(AsyncAction.Success::class.java) + assertThat(successState.selectedUser).isNull() + } + } + + @Test + fun `present - do kick user with failure`() = runTest { + val error = RuntimeException("Test error") + val room = aJoinedRoom( + kickUserResult = Result.failure(error), + ) + createRoomMemberModerationPresenter(room = room).test { + val initialState = awaitState() + initialState.eventSink( + RoomMemberModerationEvents.ProcessAction( + targetUser = targetUser, + action = ModerationAction.KickUser + ) + ) + skipItems(2) + initialState.eventSink(InternalRoomMemberModerationEvents.DoKickUser("Reason")) + skipItems(1) + val loadingState = awaitState() + assertThat(loadingState.kickUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java) + val failureState = awaitState() + assertThat(failureState.kickUserAsyncAction).isInstanceOf(AsyncAction.Failure::class.java) + } + } + + @Test + fun `present - reset clears all async actions and selected user`() = runTest { + createRoomMemberModerationPresenter(room = aJoinedRoom()).test { + val initialState = awaitState() + initialState.eventSink( + RoomMemberModerationEvents.ProcessAction(targetUser = targetUser, action = ModerationAction.BanUser) + ) + skipItems(2) + initialState.eventSink(InternalRoomMemberModerationEvents.Reset) + skipItems(1) + val resetState = awaitState() + assertThat(resetState.selectedUser).isNull() + assertThat(resetState.banUserAsyncAction).isEqualTo(AsyncAction.Uninitialized) + } + } + + private fun aJoinedRoom( + canKick: Boolean = false, + canBan: Boolean = false, + myUserRole: RoomMember.Role = RoomMember.Role.USER, + kickUserResult: Result = Result.success(Unit), + banUserResult: Result = Result.success(Unit), + unBanUserResult: Result = Result.success(Unit), + targetRoomMember: RoomMember? = null, + ): JoinedRoom { + return FakeJoinedRoom( + kickUserResult = { _, _ -> kickUserResult }, + banUserResult = { _, _ -> banUserResult }, + unBanUserResult = { _, _ -> unBanUserResult }, + baseRoom = FakeBaseRoom( + canBanResult = { _ -> Result.success(canBan) }, + canKickResult = { _ -> Result.success(canKick) }, + userRoleResult = { Result.success(myUserRole) }, + ), + ).apply { + val roomMembers = listOfNotNull(targetRoomMember).toPersistentList() + givenRoomMembersState(state = RoomMembersState.Ready(roomMembers)) + } + } + + private fun TestScope.createRoomMemberModerationPresenter( + room: JoinedRoom, + dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), + analyticsService: AnalyticsService = FakeAnalyticsService(), + ): RoomMemberModerationPresenter { + return RoomMemberModerationPresenter( + room = room, + dispatchers = dispatchers, + analyticsService = analyticsService, + ) + } + + private suspend fun TurbineTestContext.awaitState(): InternalRoomMemberModerationState { + return awaitItem() as InternalRoomMemberModerationState + } +} diff --git a/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationViewTest.kt b/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationViewTest.kt new file mode 100644 index 0000000000..d17b7579b6 --- /dev/null +++ b/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationViewTest.kt @@ -0,0 +1,226 @@ +/* + * 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.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.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.matrix.api.user.MatrixUser +import io.element.android.libraries.testtags.TestTags +import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParams +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnceWithTwoParams +import io.element.android.tests.testutils.pressTag +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class RoomMemberModerationViewTest { + @get:Rule val rule = createAndroidComposeRule() + + @Test + fun `clicking on display profile action calls onSelectAction`() { + val user = anAlice() + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnceWithTwoParams(ModerationAction.DisplayProfile, user) { callback -> + rule.setRoomMemberModerationView( + aRoomMembersModerationState( + selectedUser = user, + actions = listOf( + ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true), + ), + eventSink = eventsRecorder + ), + onSelectAction = callback + ) + rule.clickOn(R.string.screen_bottom_sheet_manage_room_member_member_user_info) + } + } + + @Test + fun `clicking on kick user action calls onSelectAction`() { + val user = anAlice() + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnceWithTwoParams(ModerationAction.KickUser, user) { callback -> + rule.setRoomMemberModerationView( + aRoomMembersModerationState( + selectedUser = user, + actions = listOf( + ModerationActionState(action = ModerationAction.KickUser, isEnabled = true), + ), + eventSink = eventsRecorder + ), + onSelectAction = callback + ) + rule.clickOn(R.string.screen_bottom_sheet_manage_room_member_remove) + // Gives time for bottomsheet to hide + rule.mainClock.advanceTimeBy(1_000) + } + } + + @Test + fun `clicking on ban user action calls onSelectAction`() { + val user = anAlice() + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnceWithTwoParams(ModerationAction.BanUser, user) { callback -> + rule.setRoomMemberModerationView( + aRoomMembersModerationState( + selectedUser = user, + actions = listOf( + ModerationActionState(action = ModerationAction.BanUser, isEnabled = true), + ), + eventSink = eventsRecorder + ), + onSelectAction = callback + ) + rule.clickOn(R.string.screen_bottom_sheet_manage_room_member_ban) + // Gives time for bottomsheet to hide + rule.mainClock.advanceTimeBy(1_000) + } + } + + @Test + fun `clicking on unban user action calls onSelectAction`() { + val user = anAlice() + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnceWithTwoParams(ModerationAction.UnbanUser, user) { callback -> + rule.setRoomMemberModerationView( + aRoomMembersModerationState( + selectedUser = user, + actions = listOf( + ModerationActionState(action = ModerationAction.UnbanUser, isEnabled = true), + ), + eventSink = eventsRecorder + ), + onSelectAction = callback + ) + rule.clickOn(R.string.screen_bottom_sheet_manage_room_member_unban) + // Gives time for bottomsheet to hide + rule.mainClock.advanceTimeBy(1_000) + } + } + + @Test + fun `clicking submit on kick confirmation dialog sends DoKickUser event`() { + val eventsRecorder = EventsRecorder() + rule.setRoomMemberModerationView( + aRoomMembersModerationState( + selectedUser = anAlice(), + kickUserAsyncAction = AsyncAction.ConfirmingNoParams, + eventSink = eventsRecorder + ), + ) + rule.pressTag(TestTags.dialogPositive.value) + eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.DoKickUser(reason = "")) + } + + @Test + fun `clicking dismiss on kick confirmation dialog sends Reset event`() { + val eventsRecorder = EventsRecorder() + rule.setRoomMemberModerationView( + aRoomMembersModerationState( + selectedUser = anAlice(), + kickUserAsyncAction = AsyncAction.ConfirmingNoParams, + eventSink = eventsRecorder + ), + ) + rule.pressTag(TestTags.dialogNegative.value) + eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.Reset) + } + + @Test + fun `clicking submit on ban confirmation dialog sends DoBanUser event`() { + val eventsRecorder = EventsRecorder() + rule.setRoomMemberModerationView( + aRoomMembersModerationState( + selectedUser = anAlice(), + banUserAsyncAction = AsyncAction.ConfirmingNoParams, + eventSink = eventsRecorder + ), + ) + rule.pressTag(TestTags.dialogPositive.value) + eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.DoBanUser(reason = "")) + } + + @Test + fun `clicking dismiss on ban confirmation dialog sends Reset event`() { + val eventsRecorder = EventsRecorder() + rule.setRoomMemberModerationView( + aRoomMembersModerationState( + selectedUser = anAlice(), + banUserAsyncAction = AsyncAction.ConfirmingNoParams, + eventSink = eventsRecorder + ), + ) + rule.pressTag(TestTags.dialogNegative.value) + eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.Reset) + } + + @Test + fun `clicking confirm on unban confirmation dialog sends DoUnbanUser event`() { + val eventsRecorder = EventsRecorder() + rule.setRoomMemberModerationView( + aRoomMembersModerationState( + selectedUser = anAlice(), + unbanUserAsyncAction = AsyncAction.ConfirmingNoParams, + eventSink = eventsRecorder + ), + ) + rule.pressTag(TestTags.dialogPositive.value) + eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.DoUnbanUser) + } + + @Test + fun `clicking dismiss on unban confirmation dialog sends Reset event`() { + val eventsRecorder = EventsRecorder() + rule.setRoomMemberModerationView( + aRoomMembersModerationState( + selectedUser = anAlice(), + unbanUserAsyncAction = AsyncAction.ConfirmingNoParams, + eventSink = eventsRecorder + ), + ) + rule.pressTag(TestTags.dialogNegative.value) + eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.Reset) + } + + @Test + fun `disabled actions are not clickable`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + rule.setRoomMemberModerationView( + aRoomMembersModerationState( + selectedUser = anAlice(), + actions = listOf( + ModerationActionState(action = ModerationAction.KickUser, isEnabled = false), + ), + eventSink = eventsRecorder + ), + ) + rule.clickOn(R.string.screen_bottom_sheet_manage_room_member_remove) + } +} + +private fun AndroidComposeTestRule.setRoomMemberModerationView( + state: InternalRoomMemberModerationState, + onSelectAction: (ModerationAction, MatrixUser) -> Unit = EnsureNeverCalledWithTwoParams(), +) { + setContent { + RoomMemberModerationView( + state = state, + onSelectAction = onSelectAction, + ) + } +} 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 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 cfbd0daa7d..2fbe44f312 100644 --- a/libraries/ui-strings/src/main/res/values-be/translations.xml +++ b/libraries/ui-strings/src/main/res/values-be/translations.xml @@ -294,15 +294,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 c6c48baf17..6a3d4a861f 100644 --- a/libraries/ui-strings/src/main/res/values-cs/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml @@ -348,23 +348,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…" - "Zrušit vykázání z místnosti" - "Zrušit vykázání" - "Pokud by byli pozváni, mohli by se znovu připojit do místnosti" - "Opravdu chcete zrušit vykázání tohoto člena?" - "Rušení vykázá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 691ad643f4..c6747aca86 100644 --- a/libraries/ui-strings/src/main/res/values-cy/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cy/translations.xml @@ -363,23 +363,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…" - "Dad-wahardd o\'r ystafell" - "Dad-wahardd" - "Bydden nhw\'n gallu ymuno â\'r ystafell eto os fydd rhywun yn eu gwahodd" - "Ydych chi\'n siŵr eich bod chi eisiau dadwahardd yr aelod hwn?" - "Dad-wahardd %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 05084459a9..afcc1f784f 100644 --- a/libraries/ui-strings/src/main/res/values-de/translations.xml +++ b/libraries/ui-strings/src/main/res/values-de/translations.xml @@ -343,23 +343,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." - "Möchten Sie diesen Nutzer wirklich sperren?" - "%1$s wird gesperrt." - "Entfernen" - "Sie können diesen Raum wieder betreten, wenn sie eingeladen werden." - "Möchten Sie dieses Mitglied wirklich entfernen?" - "Nutzerprofil anzeigen" - "Mitglied entfernen" - "Mitglied entfernen und für die Zukunft sperren?" - "%1$s wird entfernt." - "Sperre für diesen Chatroom aufheben" - "Sperre aufheben" - "Sie könnten den Chatroom wieder betreten, wenn sie wieder eingeladen würden." - "Möchten Sie die Sperre dieses Mitglieds wirklich aufheben?" - "Sperre für %1$s aufheben" "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 5a318772b0..efadb6753d 100644 --- a/libraries/ui-strings/src/main/res/values-el/translations.xml +++ b/libraries/ui-strings/src/main/res/values-el/translations.xml @@ -343,18 +343,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 a09dbb1858..50fbc4479d 100644 --- a/libraries/ui-strings/src/main/res/values-es/translations.xml +++ b/libraries/ui-strings/src/main/res/values-es/translations.xml @@ -320,15 +320,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 93a75c7966..9547e7663a 100644 --- a/libraries/ui-strings/src/main/res/values-et/translations.xml +++ b/libraries/ui-strings/src/main/res/values-et/translations.xml @@ -343,23 +343,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…" - "Eemalda suhtluskeeld jututoas" - "Eemalda suhtluskeeld" - "Ta võib kutse saamisel liituda jututoaga uuesti" - "Kas oled kindel, et soovid selle liikme suhtluskeelu eemaldada?" - "Eemaldame suhtluskeelu kasutajalt %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 a0fb445a4e..c3c2c12a1c 100644 --- a/libraries/ui-strings/src/main/res/values-eu/translations.xml +++ b/libraries/ui-strings/src/main/res/values-eu/translations.xml @@ -322,14 +322,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 d482f465eb..4c3be6c5c3 100644 --- a/libraries/ui-strings/src/main/res/values-fi/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fi/translations.xml @@ -343,23 +343,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…" - "Poista porttikielto huoneesta" - "Poista porttikielto" - "He voivat liittyä huoneeseen uudelleen, jos heidät kutsutaan" - "Haluatko varmasti poistaa tämän jäsenen porttikiellon?" - "Poistetaan käyttäjän %1$s porttikieltoa" "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 376a757ee5..d15958d55a 100644 --- a/libraries/ui-strings/src/main/res/values-fr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fr/translations.xml @@ -343,23 +343,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…" - "Débannir du salon" - "Débannir" - "L’utilisateur pourra à nouveau rejoindre le salon s’il est invité." - "Êtes-vous sûr de vouloir débannir cet utilisateur?" - "Débannissement de %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 a528b0b066..ab14bca3b7 100644 --- a/libraries/ui-strings/src/main/res/values-hu/translations.xml +++ b/libraries/ui-strings/src/main/res/values-hu/translations.xml @@ -343,23 +343,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…" - "Visszaengedés a szobába" - "Kitiltás visszavonása" - "Újra beléphetnek a szobába, ha meghívják őket." - "Biztos, hogy feloldja a felhasználó kitiltását?" - "%1$s kitiltásának feloldá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 c70f67ce04..722bf149ac 100644 --- a/libraries/ui-strings/src/main/res/values-in/translations.xml +++ b/libraries/ui-strings/src/main/res/values-in/translations.xml @@ -307,15 +307,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 c185a0745e..c78465247f 100644 --- a/libraries/ui-strings/src/main/res/values-it/translations.xml +++ b/libraries/ui-strings/src/main/res/values-it/translations.xml @@ -330,15 +330,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 4f6febc29e..5688760fc3 100644 --- a/libraries/ui-strings/src/main/res/values-ka/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ka/translations.xml @@ -251,15 +251,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 ccd0201788..573107cc01 100644 --- a/libraries/ui-strings/src/main/res/values-nb/translations.xml +++ b/libraries/ui-strings/src/main/res/values-nb/translations.xml @@ -343,23 +343,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…" - "Fjern utestengelsen fra rommet" - "Opphev utestengelsen" - "De vil kunne bli med i rommet igjen hvis de blir invitert" - "Er du sikker på at du vil oppheve utestengelsen av dette medlemmet?" - "Oppheve utestengelsen av %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 e2e0bed50b..1066ee7e79 100644 --- a/libraries/ui-strings/src/main/res/values-nl/translations.xml +++ b/libraries/ui-strings/src/main/res/values-nl/translations.xml @@ -296,15 +296,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 bdadf95799..76b3177b6a 100644 --- a/libraries/ui-strings/src/main/res/values-pl/translations.xml +++ b/libraries/ui-strings/src/main/res/values-pl/translations.xml @@ -348,18 +348,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 f73041b2a7..d8fddecca2 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 @@ -343,18 +343,6 @@ Você tem certeza de que deseja continuar?" "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" - "Remover" - "Eles poderão entrar nesta sala novamente se forem convidados." - "Tem certeza de que deseja remover este membro?" - "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." "As legendas podem não ser visíveis para pessoas que usam aplicativos mais antigos." "Falha ao processar mídia para upload. 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 d1cf2a258f..d8931a4a99 100644 --- a/libraries/ui-strings/src/main/res/values-pt/translations.xml +++ b/libraries/ui-strings/src/main/res/values-pt/translations.xml @@ -343,23 +343,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" - "Remover" - "Poderão entrar na sala novamente se convidados." - "Tens a certeza que queres remover este membro?" - "Ver perfil" - "Remover da sala" - "Remover participante e proibir que entre no futuro?" - "A remover %1$s…" - "Anular banimento da sala" - "Anular banimento" - "Poderão entrar novamente na sala se forem convidados" - "Tens a certeza que queres anular o banimento deste utilizador?" - "A anular banimento de %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 eb363c2598..76771a4c9f 100644 --- a/libraries/ui-strings/src/main/res/values-ro/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ro/translations.xml @@ -299,15 +299,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 d2af544e53..67a9e3b03a 100644 --- a/libraries/ui-strings/src/main/res/values-ru/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ru/translations.xml @@ -348,17 +348,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 3f00756e10..fa2ccf4b58 100644 --- a/libraries/ui-strings/src/main/res/values-sk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml @@ -348,23 +348,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…" - "Zrušiť zákaz prístupu do miestnosti" - "Zrušiť zákaz" - "V prípade pozvania by sa mohli opäť pripojiť k miestnosti" - "Naozaj chcete zrušiť zablokovanie tohto člena?" - "Zrušenie zákazu pre %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 75333d408e..2733eed812 100644 --- a/libraries/ui-strings/src/main/res/values-sv/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sv/translations.xml @@ -343,23 +343,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 …" - "Avbanna från rummet" - "Avbanna" - "De skulle kunna gå med i rummet igen om de blev inbjudna" - "Är du säker på att du vill avbanna den här medlemmen?" - "Avbannar %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 d73e1d1dec..40bb3fe7e4 100644 --- a/libraries/ui-strings/src/main/res/values-tr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-tr/translations.xml @@ -320,15 +320,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 69c5104375..b317dd24d4 100644 --- a/libraries/ui-strings/src/main/res/values-uk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-uk/translations.xml @@ -348,23 +348,6 @@ "Вітаю, поспілкуйтеся зі мною в %1$s: %2$s" "%1$s Android" "Повідомити про ваду за допомогою Rageshake" - "Вилучити й заблокувати учасника" - "Заблокувати" - "Він не зможе приєднатися до цієї кімнати знову, якщо його запросять." - "Ви точно хочете заблокувати цього користувача?" - "Блокування %1$s" - "Вилучити" - "Вони зможуть знову приєднатися до цієї кімнати, якщо їх запросять." - "Ви дійсно хочете вилучити цього учасника?" - "Переглянути профіль" - "Вилучити з кімнати" - "Вилучити учасника та заборонити приєднання в майбутньому?" - "Вилучення %1$s…" - "Розблокувати в кімнаті" - "Розблокувати" - "Вони зможуть знову приєднатися до кімнати, якщо їх запросять" - "Ви впевнені, що хочете розблокувати цього учасника?" - "Розблокування %1$s" "Не вдалося вибрати медіафайл, спробуйте ще раз." "Користувачі старих застосунків можуть не бачити підписи." "Не вдалося обробити медіафайл для завантаження, спробуйте ще раз." diff --git a/libraries/ui-strings/src/main/res/values-ur/translations.xml b/libraries/ui-strings/src/main/res/values-ur/translations.xml index 56864f29b1..e71a5e2014 100644 --- a/libraries/ui-strings/src/main/res/values-ur/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ur/translations.xml @@ -271,15 +271,6 @@ "ارے، مجھ سے %1$s پر بات کریں: %2$s" "%1$s Android" "خطاء کی اطلاع دینے کیلئے غصے سے ہلائیں" - "کمرے سے محظور کریں" - "محظور کریں" - "اگر وہ مدعو کیا گیا تو وہ دوبارہ اس کمرے میں شامل نہیں ہوسکیں گے۔" - "کیا آپ کو یقین ہے کہ آپ اس رکن کو محظور کرنا چاہتے ہیں؟" - "%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 eb7ca3eaf2..835b89bc19 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 @@ -338,18 +338,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 7884888483..c43a8bf1da 100644 --- a/libraries/ui-strings/src/main/res/values-zh/translations.xml +++ b/libraries/ui-strings/src/main/res/values-zh/translations.xml @@ -330,18 +330,6 @@ "嗨!请通过 %1$s 与我联系:%2$s" "%1$s Android" "摇一摇以报错" - "移除并封禁成员" - "封禁" - "即使受到邀请,他们也无法再次加入聊天室。" - "您确定要封禁该成员吗?" - "封禁 %1$s" - "移除" - "如果受到邀请,他们可以重新加入聊天室。" - "您确定要移除此成员吗?" - "查看个人资料" - "从聊天室移除" - "删除成员并禁止重新加入?" - "正在移除 %1$s……" "选择媒体失败,请重试。" "使用旧版应用程序的用户可能无法看到字幕。" "处理要上传的媒体失败,请重试。" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 90100bbcd7..1548788ce4 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -343,23 +343,6 @@ Are you sure you want to continue?" "Hey, talk to me on %1$s: %2$s" "%1$s Android" "Rageshake to report bug" - "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…" - "Unban from room" - "Unban" - "They would be able to join the room again if invited" - "Are you sure you want to unban this member?" - "Unbanning %1$s" "Failed selecting media, please try again." "Captions might not be visible to people using older apps." "Failed processing media to upload, please try again." diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_4_en.png index 60ec9cc17b..fb57633a41 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f90a8ecae26714e76d5536682716a15aefc3d4c8836617add9a0ea946e7d242 -size 31486 +oid sha256:48b327608bde4f150450306fb3a3b2508dd437b43e301fda58a8bc279ba093f3 +size 31326 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_4_en.png index 0581885f19..20ae4c9cdd 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:34cd9f4558efc143b4ab3bec96342a42cfad6906da0d46e75d78db36b7e59bd6 -size 30242 +oid sha256:c31eaa5cbb8850fd523915a401e3b9ade2a58845b76cf2c7c4fec78685479159 +size 30164 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_10_en.png deleted file mode 100644 index 1b6fb4bab8..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_10_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:96a867cb12498cbdc97957bee07855dfaa13602baddaf933aff2b666ef4c7650 -size 3642 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_1_en.png deleted file mode 100644 index 408db0efb4..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b6cbd2503dba04b18d854d0642f4da2cea12bfbc280760ab3306021b2de00302 -size 21382 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_2_en.png deleted file mode 100644 index 9b35d63d7f..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_2_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d913c6d23a74ba9a117ccc55922f556b0a51deabcc030a7942bdede9297ae624 -size 24805 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_7_en.png deleted file mode 100644 index 1b6fb4bab8..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_7_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:96a867cb12498cbdc97957bee07855dfaa13602baddaf933aff2b666ef4c7650 -size 3642 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_8_en.png deleted file mode 100644 index 6f43e3db48..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_8_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f83d60c7e1d963ccef24668c219bd9df36494dc8bd8f57af7aa63a3b73ee6882 -size 7545 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_9_en.png deleted file mode 100644 index d220520bfc..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_9_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b323796ccdf27e5c445aa02e3f7b73f65df961dd88f3ee480efcba14d8038281 -size 19426 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_10_en.png deleted file mode 100644 index d6fd8eeb70..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_10_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5bb36ccd718f3fec5b04f1bc812dc7718b5ea7fa4619c8b031466297a8d016fd -size 3659 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_1_en.png deleted file mode 100644 index 32f1f4e98c..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4cb84e446734d3b36d566ad42e0143c0733ceabe55acf4cb58a7ed28ee29be2e -size 20125 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_2_en.png deleted file mode 100644 index cca7dcc511..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_2_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4da58fd502c79c18ddd82dc3be4f7c4c26fb1191d9ed499e447af061bd5a6fc5 -size 23445 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_7_en.png deleted file mode 100644 index d6fd8eeb70..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_7_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5bb36ccd718f3fec5b04f1bc812dc7718b5ea7fa4619c8b031466297a8d016fd -size 3659 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_8_en.png deleted file mode 100644 index 08349f1edb..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_8_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ba213b12d181b04695b9271670cb7d96ce83dfccc3c2031ee002ba1cf180cc8d -size 6408 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_9_en.png deleted file mode 100644 index 51ce6077b5..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_9_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f9def02bcb5784681dc26327791510a9a25f0772644cdae363d7a4b90e69067c -size 17479 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Day_0_en.png index 7d81d8d062..d180111d1c 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:42c0b3ad98e83591ac2defcd17a17e4250d00b5704fbb9e3fde425e2990b4613 -size 32545 +oid sha256:b0652d08a2c59e59853b3ee0625a2cee62cf68e8542de16f26e7174f18af620e +size 11465 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Day_1_en.png index e07d1a7cd9..9627b16e6c 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a60abe6da2ae9b7e2a6eb6ae3a8057d1476a8f76d867288436cfbd7a063002c8 -size 32840 +oid sha256:2350a560fcc78a3d1b22cd235746ac03b46eee2912184dbbe62683540dfee2b4 +size 11704 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Day_2_en.png index 77d18480f4..d180111d1c 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:69fb66dd35d7cccf07e97135defc0dc46b9ff29ec33c6ed6b9ff3b6b0be5d873 -size 22036 +oid sha256:b0652d08a2c59e59853b3ee0625a2cee62cf68e8542de16f26e7174f18af620e +size 11465 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Night_0_en.png index a182629276..70b26f1264 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:daba36e47b9f2d685a7450265dba8abc687b8256df10d9c1445202c80345c52c -size 31877 +oid sha256:4a64ec55a66dac97601880e5211153c5a25985fe0184186ab3c8d8dbebd9973d +size 10834 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Night_1_en.png index 1e228e66c5..46b47ed015 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fbf5e7155b878f39e3cc561c50b89323ac9fcef9dbc7364e62cf95cc0ad91e81 -size 32321 +oid sha256:9bee4a7a63485b54555a203b8e4fc557e504b3efacb91d1c7c082647848c8c45 +size 11051 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Night_2_en.png index 61ce9c07e3..70b26f1264 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members_RoomMemberListViewBanned_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78370686e4fa83ed1f2951a7d71738531b09b3808c66729bf6544d5b2b9a6c12 -size 20944 +oid sha256:4a64ec55a66dac97601880e5211153c5a25985fe0184186ab3c8d8dbebd9973d +size 10834 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_0_en.png rename to tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_1_en.png new file mode 100644 index 0000000000..2d38dfd46b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0c6367c53677d284694ed3e06f8d548a33464822094445344aa7cbd8bb3e88e +size 20687 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_2_en.png new file mode 100644 index 0000000000..15a24a06d9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7fbdf555e12892fa89fa73e77d86f7f2f1beab8328fd0b22498e37ee5f966c46 +size 23648 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_3_en.png new file mode 100644 index 0000000000..b6be401d79 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:02d7a5e6169a31c546745565e65766cedb4e70faa00ff73675a5781da96cefaa +size 23770 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_4_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_3_en.png rename to tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_4_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_5_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_4_en.png rename to tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_5_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_6_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_5_en.png rename to tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_6_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_7_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_6_en.png rename to tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_7_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_0_en.png rename to tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_1_en.png new file mode 100644 index 0000000000..a335623384 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34432e8a0eaecd71b4f0db3eeb4914a1e0450652d3c4bdc6d57745d530c51128 +size 19280 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_2_en.png new file mode 100644 index 0000000000..92014e1c1c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b3c8ed9cea0b3525ef565cbd34b3f7014b632f0c3eee9ebfc037d3ae08c44929 +size 22262 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_3_en.png new file mode 100644 index 0000000000..52dd3b4301 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a02fe75a7c5489a1e2020169db6ac50c117db968f111ce863ced5a2f59c08d1 +size 22372 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_4_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_3_en.png rename to tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_4_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_5_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_4_en.png rename to tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_5_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_6_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_5_en.png rename to tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_6_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_7_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_6_en.png rename to tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_7_en.png diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 00b51c1924..b299c39ef7 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -320,6 +320,12 @@ "includeRegex" : [ "screen\\.report_room\\..*" ] + }, + { + "name" : ":features:roommembermoderation:impl", + "includeRegex" : [ + "screen\\.bottom_sheet\\.manage_room_member\\..*" + ] } ] }