Merge pull request #4779 from element-hq/feature/fga/user_moderation_bottomsheet
Change : RoomMember moderation
This commit is contained in:
commit
4539474627
144 changed files with 1945 additions and 1672 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<ReadReceiptBottomSheetState>,
|
||||
private val pinnedMessagesBannerPresenter: Presenter<PinnedMessagesBannerState>,
|
||||
private val roomCallStatePresenter: Presenter<RoomCallState>,
|
||||
private val roomMemberModerationPresenter: Presenter<RoomMemberModerationState>,
|
||||
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) }
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<RoomMemberModerationState> = 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,
|
||||
|
|
|
|||
|
|
@ -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<MessagesEvents>(expectEvents = false)
|
||||
fun `clicking on the avatar of the sender of an Event emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<MessagesEvents>()
|
||||
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<TimelineItem.Event>().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<MessagesEvents>(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<MessagesEvents>()
|
||||
val state = aMessagesState(eventSink = eventsRecorder)
|
||||
val timelineEvent = state.timelineState.timelineItems.filterIsInstance<TimelineItem.Event>().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
|
||||
|
|
|
|||
|
|
@ -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 <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.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(),
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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 <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.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(),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<RoomMembersModerationState>
|
||||
}
|
||||
|
|
@ -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<Plugin>,
|
||||
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<Callback>()
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<RoomMembersModerationState>,
|
||||
private val roomMembersModerationPresenter: Presenter<RoomMemberModerationState>,
|
||||
private val encryptionService: EncryptionService,
|
||||
@Assisted private val navigator: RoomMemberListNavigator,
|
||||
) : Presenter<RoomMemberListState> {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(navigator: RoomMemberListNavigator): RoomMemberListPresenter
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): RoomMemberListState {
|
||||
var roomMembers: AsyncData<RoomMembers> 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<UserId, IdentityState>()) {
|
||||
|
|
@ -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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<AsyncData<RoomMembers>>,
|
||||
val isSearchActive: Boolean,
|
||||
val canInvite: Boolean,
|
||||
val moderationState: RoomMembersModerationState,
|
||||
val moderationState: RoomMemberModerationState,
|
||||
val eventSink: (RoomMemberListEvents) -> Unit,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Room
|
|||
),
|
||||
)
|
||||
),
|
||||
moderationState = aRoomMembersModerationState(canDisplayBannedUsers = true),
|
||||
moderationState = aRoomMemberModerationState(),
|
||||
),
|
||||
aRoomMemberListState(
|
||||
roomMembers = AsyncData.Loading(
|
||||
|
|
@ -101,7 +101,7 @@ internal class RoomMemberListStateBannedProvider : PreviewParameterProvider<Room
|
|||
),
|
||||
)
|
||||
),
|
||||
moderationState = aRoomMembersModerationState(canDisplayBannedUsers = true),
|
||||
moderationState = aRoomMemberModerationState(),
|
||||
),
|
||||
aRoomMemberListState(
|
||||
roomMembers = AsyncData.Success(
|
||||
|
|
@ -111,7 +111,7 @@ internal class RoomMemberListStateBannedProvider : PreviewParameterProvider<Room
|
|||
banned = persistentListOf(),
|
||||
)
|
||||
),
|
||||
moderationState = aRoomMembersModerationState(canDisplayBannedUsers = true),
|
||||
moderationState = aRoomMemberModerationState(),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -119,7 +119,7 @@ internal class RoomMemberListStateBannedProvider : PreviewParameterProvider<Room
|
|||
internal fun aRoomMemberListState(
|
||||
roomMembers: AsyncData<RoomMembers> = AsyncData.Loading(),
|
||||
searchResults: SearchBarResultState<AsyncData<RoomMembers>> = 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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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<RoomMembersModerationState> {
|
||||
private var selectedMember by mutableStateOf<RoomMember?>(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<Unit>) }
|
||||
val banUserAsyncAction =
|
||||
remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction<Unit>) }
|
||||
val unbanUserAsyncAction =
|
||||
remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction<Unit>) }
|
||||
|
||||
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<AsyncAction<Unit>>,
|
||||
) = 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<AsyncAction<Unit>>,
|
||||
) = 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<AsyncAction<Unit>>,
|
||||
) = runActionAndWaitForMembershipChange(unbanUserAction) {
|
||||
analyticsService.capture(RoomModeration(RoomModeration.Action.UnbanMember))
|
||||
room.unbanUser(userId)
|
||||
}
|
||||
|
||||
private fun <T> CoroutineScope.runActionAndWaitForMembershipChange(
|
||||
action: MutableState<AsyncAction<T>>,
|
||||
block: suspend () -> Result<T>
|
||||
) {
|
||||
launch(dispatchers.io) {
|
||||
action.runUpdatingState {
|
||||
val result = block()
|
||||
if (result.isSuccess) {
|
||||
room.membersStateFlow.drop(1).take(1)
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ModerationAction>,
|
||||
val kickUserAsyncAction: AsyncAction<Unit>,
|
||||
val banUserAsyncAction: AsyncAction<Unit>,
|
||||
val unbanUserAsyncAction: AsyncAction<Unit>,
|
||||
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
|
||||
}
|
||||
|
|
@ -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<RoomMembersModerationState> {
|
||||
override val values: Sequence<RoomMembersModerationState>
|
||||
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<ModerationAction> = emptyList(),
|
||||
kickUserAsyncAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
banUserAsyncAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
unbanUserAsyncAction: AsyncAction<Unit> = 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,
|
||||
)
|
||||
|
|
@ -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<RoomMembersModerationEvents>()
|
||||
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<RoomMemberModerationState> = Presenter {
|
||||
aRoomMemberModerationState()
|
||||
},
|
||||
) = RoomMemberListPresenter(
|
||||
room = joinedRoom,
|
||||
roomMemberListDataSource = roomMemberListDataSource,
|
||||
coroutineDispatchers = coroutineDispatchers,
|
||||
roomMembersModerationPresenter = { roomMembersModerationStateLambda() },
|
||||
roomMembersModerationPresenter = roomMemberModerationPresenter,
|
||||
encryptionService = encryptedService,
|
||||
navigator = navigator,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<UserId, String?, Result<Unit>> { _, _ -> 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<UserId, String?, Result<Unit>> { _, _ -> 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ComponentActivity>()
|
||||
|
||||
@Ignore("This test is not passing yet, need to investigate")
|
||||
@Test
|
||||
fun `clicking on back emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<RoomMembersModerationEvents>()
|
||||
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<RoomMembersModerationEvents>(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<RoomMembersModerationEvents>()
|
||||
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<RoomMembersModerationEvents>()
|
||||
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<RoomMembersModerationEvents>()
|
||||
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<RoomMembersModerationEvents>()
|
||||
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<RoomMembersModerationEvents>()
|
||||
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<RoomMembersModerationEvents>()
|
||||
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<RoomMembersModerationEvents>()
|
||||
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<RoomMembersModerationEvents>()
|
||||
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<RoomMembersModerationEvents>()
|
||||
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<RoomMembersModerationEvents>()
|
||||
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 <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomMembersModerationView(
|
||||
state: RoomMembersModerationState,
|
||||
onDisplayMemberProfile: (UserId) -> Unit = EnsureNeverCalledWithParam()
|
||||
) {
|
||||
setContent {
|
||||
RoomMembersModerationView(
|
||||
state = state,
|
||||
onDisplayMemberProfile = onDisplayMemberProfile,
|
||||
)
|
||||
}
|
||||
}
|
||||
21
features/roommembermoderation/api/build.gradle.kts
Normal file
21
features/roommembermoderation/api/build.gradle.kts
Normal file
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
47
features/roommembermoderation/impl/build.gradle.kts
Normal file
47
features/roommembermoderation/impl/build.gradle.kts
Normal file
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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<ModerationActionState>,
|
||||
val kickUserAsyncAction: AsyncAction<Unit>,
|
||||
val banUserAsyncAction: AsyncAction<Unit>,
|
||||
val unbanUserAsyncAction: AsyncAction<Unit>,
|
||||
override val eventSink: (RoomMemberModerationEvents) -> Unit,
|
||||
) : RoomMemberModerationState {
|
||||
val canDisplayActions = actions.isNotEmpty()
|
||||
}
|
||||
|
|
@ -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<InternalRoomMemberModerationState> {
|
||||
override val values: Sequence<InternalRoomMemberModerationState>
|
||||
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<ModerationActionState> = emptyList(),
|
||||
kickUserAsyncAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
banUserAsyncAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
unbanUserAsyncAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
eventSink: (RoomMemberModerationEvents) -> Unit = {},
|
||||
) = InternalRoomMemberModerationState(
|
||||
canKick = canKick,
|
||||
canBan = canBan,
|
||||
selectedUser = selectedUser,
|
||||
actions = actions.toPersistentList(),
|
||||
kickUserAsyncAction = kickUserAsyncAction,
|
||||
banUserAsyncAction = banUserAsyncAction,
|
||||
unbanUserAsyncAction = unbanUserAsyncAction,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
|
@ -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<RoomMemberModerationState> {
|
||||
@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<Unit>) }
|
||||
val banUserAsyncAction =
|
||||
remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction<Unit>) }
|
||||
val unbanUserAsyncAction =
|
||||
remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction<Unit>) }
|
||||
var selectedUser by remember {
|
||||
mutableStateOf<MatrixUser?>(null)
|
||||
}
|
||||
val moderationActions = remember { mutableStateOf(persistentListOf<ModerationActionState>()) }
|
||||
|
||||
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<ModerationActionState> {
|
||||
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<AsyncAction<Unit>>,
|
||||
) = 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<AsyncAction<Unit>>,
|
||||
) = 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<AsyncAction<Unit>>,
|
||||
) = runActionAndWaitForMembershipChange(unbanUserAction) {
|
||||
analyticsService.capture(RoomModeration(RoomModeration.Action.UnbanMember))
|
||||
room.unbanUser(userId = userId)
|
||||
}
|
||||
|
||||
private fun <T> CoroutineScope.runActionAndWaitForMembershipChange(
|
||||
action: MutableState<AsyncAction<T>>,
|
||||
block: suspend () -> Result<T>
|
||||
) {
|
||||
launch(dispatchers.io) {
|
||||
action.runUpdatingState {
|
||||
val result = block()
|
||||
if (result.isSuccess) {
|
||||
room.membersStateFlow.drop(1).take(1)
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ModerationAction>,
|
||||
onSelectAction: (ModerationAction) -> Unit,
|
||||
user: MatrixUser,
|
||||
actions: ImmutableList<ModerationActionState>,
|
||||
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 = { _, _ ->
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<RoomMemberModerationState>
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Выдаліць і заблакіраваць удзельніка"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Заблакіраваць"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Яны не змогуць зноў далучыцца да гэтага пакоя, калі іх запросяць."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Вы ўпэўнены, што хочаце заблакіраваць гэтага карыстальніка?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Блакіроўка %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Прагляд профілю"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Выдаліць удзельніка з пакоя"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Выдаліць удзельніка і забараніць далучацца ў будучыні?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Выдаленне %1$s…"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Odebrat a vykázat člena"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Vykázat"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Nebudou se moci znovu připojit k této místnosti, pokud budou pozváni."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Jste si jisti, že chcete vykázat tohoto člena?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Vykazování %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Odebrat"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"Budou moci znovu vstoupit do této místnosti, pokud budou pozváni."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Opravdu chcete tohoto člena odebrat?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Zobrazit profil"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Odebrat z místnosti"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Odebrat člena a zakázat mu připojení v budoucnu?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Odstraňování %1$s…"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban">"Zrušit vykázání z místnosti"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_action">"Zrušit vykázání"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_description">"Pokud by byli pozváni, mohli by se znovu připojit do místnosti"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_title">"Opravdu chcete zrušit vykázání tohoto člena?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unbanning_user">"Rušení vykázání %1$s"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Gwahardd o ystafell"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Atal"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Fyddan nhw ddim yn gallu ymuno â\'r ystafell hon eto os cân nhw wahoddiad."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Ydych chi\'n siŵr eich bod am wahardd yr aelod hwn?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Yn gwahardd %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Tynnu"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"Fyddan nhw yn gallu ymuno â\'r ystafell hon eto os cân nhw wahoddiad."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Ydych chi\'n siŵr eich bod am ddileu\'r aelod hwn?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Gweld proffil"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Tynnu o\'r ystafell"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Dileu aelod a\'u gwahardd rhag ymuno yn y dyfodol?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Wrthi\'n dileu %1$s…"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban">"Dad-wahardd o\'r ystafell"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_action">"Dad-wahardd"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_description">"Bydden nhw\'n gallu ymuno â\'r ystafell eto os fydd rhywun yn eu gwahodd"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_title">"Ydych chi\'n siŵr eich bod chi eisiau dadwahardd yr aelod hwn?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unbanning_user">"Dad-wahardd %1$s"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Mitglied entfernen und sperren"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Sperren"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Sie können dem Raum nicht mehr beitreten, selbst wenn sie eingeladen werden."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Möchten Sie diesen Nutzer wirklich sperren?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"%1$s wird gesperrt."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Entfernen"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"Sie können diesen Raum wieder betreten, wenn sie eingeladen werden."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Möchten Sie dieses Mitglied wirklich entfernen?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Nutzerprofil anzeigen"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Mitglied entfernen"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Mitglied entfernen und für die Zukunft sperren?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"%1$s wird entfernt."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban">"Sperre für diesen Chatroom aufheben"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_action">"Sperre aufheben"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_description">"Sie könnten den Chatroom wieder betreten, wenn sie wieder eingeladen würden."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_title">"Möchten Sie die Sperre dieses Mitglieds wirklich aufheben?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unbanning_user">"Sperre für %1$s aufheben"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Αφαίρεση και αποκλεισμός μέλους"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Αποκλεισμός"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Δεν θα μπορεί να συμμετέχει ξανά σε αυτό το δωμάτιο εάν προσκληθεί."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Θες σίγουρα να αποκλείσεις αυτό το μέλος;"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Αποκλεισμός %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Αφαίρεση"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"Θα μπορούν να συμμετάσχουν ξανά σε αυτό το δωμάτιο εάν προσκληθούν."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Είστε βέβαιοι ότι θέλετε να αφαιρέσετε αυτό το μέλος;"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Προβολή προφίλ"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Αφαίρεση από το δωμάτιο"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Αφαίρεση μέλους και απαγόρευση συμμετοχής στο μέλλον;"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Αφαίρεση %1$s…"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Sacar y vetar a un miembro"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Vetar"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"No podrán volver a unirse a esta sala si son invitados."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"¿Estás seguro de que quieres vetar a este miembro?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Vetando a %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Echar"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"Podrá volver a unirse a esta sala si se le invita."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"¿Seguro que quieres echar a este miembro?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Ver perfil"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Sacar de la sala"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"¿Sacar al miembro y prohibirle unirse en el futuro?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Eliminando %1$s…"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban">"Eliminar veto en la sala"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_action">"Eliminar veto"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_description">"Podría volver a unirse a la sala si se le invita"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_title">"¿Seguro que quieres levantarle el veto a este miembro?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unbanning_user">"Levantando veto a %1$s"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Eemalda ja sea suhtluskeeld"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Sea suhtluskeeld"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Ta ei saa selle jututoaga liituda isegi kutse olemasolul."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Kas sa oled kindel, et soovid sellele kasutajale seada suhtluskeelu?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Seame kasutajale %1$s suhtluskeelu"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Eemalda"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"Uue kutse saamisel on tal võimalik selle jututoaga uuesti liituda."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Kas sa oled kindel, et soovid selle osaleja eemaldada?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Vaata profiili"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Eemalda kasutaja jututoast"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Kas eemaldama kasutaja ja seame talle tulevikuks suhtluskeelu?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Eemaldame kasutajat %1$s…"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban">"Eemalda suhtluskeeld jututoas"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_action">"Eemalda suhtluskeeld"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_description">"Ta võib kutse saamisel liituda jututoaga uuesti"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_title">"Kas oled kindel, et soovid selle liikme suhtluskeelu eemaldada?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unbanning_user">"Eemaldame suhtluskeelu kasutajalt %1$s"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Kendu kidea eta ezarri debekua"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Ezarri debekua"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Ziur kide honi debekua ezarri nahi diozula?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"%1$s(r)i debekua ezartzen"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Ikusi profila"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Kendu gelatik"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Kidea kendu eta etorkizunean sartzea debekatu?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"%1$s kentzen…"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"برداشت و تحریم عضو"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"تحریم"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"در صورت دعوت نمیتواند دوباره به اتاق بپیوندد."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"مطمئنید میخواهید این عضو را تحریم کنید؟"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"تحریم کردن %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"برداشتن"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"در صورت دعوت میتواند دوباره به اتاق بپیوندد."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"مطمئنید میخواهید این عضو را بردارید؟"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"دیدن نمایه"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"برداشتن از اتاق"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"برداشتن عضو و تحریم پیوستن در آینده؟"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"برداشتن %1$s…"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Poista jäsen huoneesta ja anna porttikielto"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Anna porttikielto"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"He eivät voi enää liittyä tähän huoneeseen, jos heidät kutsutaan."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Haluatko varmasti antaa tälle jäsenelle porttikiellon?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Annetaan porttikieltoa käyttäjälle %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Poista"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"He voivat liittyä tähän huoneeseen uudelleen, jos heidät kutsutaan."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Haluatko varmasti poistaa tämän jäsenen?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Näytä profiili"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Poista huoneesta"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Poistetaanko jäsen huoneesta ja kielletäänkö heitä liittymästä tulevaisuudessa?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Poistetaan käyttäjää %1$s huoneesta…"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban">"Poista porttikielto huoneesta"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_action">"Poista porttikielto"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_description">"He voivat liittyä huoneeseen uudelleen, jos heidät kutsutaan"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_title">"Haluatko varmasti poistaa tämän jäsenen porttikiellon?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unbanning_user">"Poistetaan käyttäjän %1$s porttikieltoa"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Retirer et bannir ce membre"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Bannir"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Il ne pourra pas rejoindre le salon à nouveau, même si il est invité."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Êtes-vous certain de vouloir bannir ce membre ?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Bannissement de %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Retirer"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"Cet utilisateur pourra rejoindre le salon à nouveau si il est invité."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Voulez-vous vraiment supprimer ce membre ?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Voir le profil"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Retirer le membre du salon"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Retirer le membre et interdire l’adhésion à l’avenir ?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Enlever %1$s…"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban">"Débannir du salon"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_action">"Débannir"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_description">"L’utilisateur pourra à nouveau rejoindre le salon s’il est invité."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_title">"Êtes-vous sûr de vouloir débannir cet utilisateur?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unbanning_user">"Débannissement de %1$s"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Eltávolítás és a tag kitiltása"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Kitiltás"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Többé nem csatlakozhat ehhez a szobához, akkor sem, ha meghívják."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Biztos, hogy kitiltja ezt a tagot?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"%1$s kitiltása"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Eltávolítás"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"Ehhez a szobához is csatlakozhat, ha meghívják."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Biztos, hogy eltávolítja ezt a tagot?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Profil megtekintése"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Eltávolítás a szobából"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Eltávolítja a tagot, és megtiltja a jövőbeni csatlakozást?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"%1$s eltávolítása…"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban">"Visszaengedés a szobába"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_action">"Kitiltás visszavonása"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_description">"Újra beléphetnek a szobába, ha meghívják őket."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_title">"Biztos, hogy feloldja a felhasználó kitiltását?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unbanning_user">"%1$s kitiltásának feloldása"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Keluarkan dan cekal anggota"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Cekal"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Mereka tidak akan dapat bergabung ke ruangan ini lagi jika diundang."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Apakah Anda yakin ingin mencekal anggota ini?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Mencekal %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Tampilkan profil"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Keluarkan dari ruangan"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Keluarkan pengguna dan cekal pengguna bergabung lagi di masa mendatang?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Mengeluarkan %1$s…"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Rimuovi ed escludi"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Escludi"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Non potrà entrare nuovamente in questa stanza se invitato."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Vuoi davvero escludere questo membro?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Esclusione di %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Visualizza profilo"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Rimuovi dalla stanza"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Rimuovere e vietare l\'accesso in futuro?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Rimozione di %1$s…"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"წევრის წაშლა და დაბლოკვა"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"დაბლოკვა"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"მოწვევის შემთხვევაში ამ ოთახში კვლავ გაწევრიანებას ვერ შეძლებენ."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"დარწმუნებული ხართ, რომ ამ წევრის დაბლოკვა გსურთ?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"%1$s-ს დაბლოკვა"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"პროფილის ნახვა"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"ოთახიდან გაგდება"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"გსურთ წევრის გაგდება და მომავალში გაწევრიანების აკრძალვა?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"%1$s-ს გაგდება…"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Fjern og utesteng medlem"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Utesteng"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"De vil ikke kunne bli med i dette rommet igjen hvis de blir invitert."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Er du sikker på at du vil utestenge dette medlemmet?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Utestenger %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Fjern"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"De vil kunne bli med i dette rommet igjen hvis de blir invitert."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Er du sikker på at du vil fjerne dette medlemmet?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Vis profil"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Fjern fra rommet"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Fjerne medlem og utestenge fra å bli med i fremtiden?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Fjerner %1$s…"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban">"Fjern utestengelsen fra rommet"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_action">"Opphev utestengelsen"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_description">"De vil kunne bli med i rommet igjen hvis de blir invitert"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_title">"Er du sikker på at du vil oppheve utestengelsen av dette medlemmet?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unbanning_user">"Oppheve utestengelsen av %1$s"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Lid verwijderen en verbannen"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Verbannen"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Ze kunnen niet meer toetreden tot deze kamer als ze worden uitgenodigd."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Weet je zeker dat je dit lid wilt verbannen?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"%1$s verbannen"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Profiel bekijken"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Verwijderen uit kamer"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Lid verwijderen en toekomstige deelname verbieden?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"%1$s wordt verwijderd…"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Usuń i zbanuj członka"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Zbanuj"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Nie będą mogli ponownie dołączyć do tego pokoju, jeśli zostaną zaproszeni."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Czy na pewno chcesz zbanować tego członka?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Banowanie %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Usuń"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"Będą mogli ponownie dołączyć do pokoju, jeśli zostaną zaproszeni."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Czy na pewno chcesz usunąć tego członka?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Wyświetl profil"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Usuń z pokoju"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Usunąć członka i zablokować możliwość dołączenia w przyszłości?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Usuwanie %1$s…"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Remover e banir membro"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Banir"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Eles não poderão entrar nesta sala novamente se forem convidados."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Tem certeza de que quer banir este membro?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Banindo %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Remover"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"Eles poderão entrar nesta sala novamente se forem convidados."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Tem certeza de que deseja remover este membro?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Ver perfil"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Remover da sala"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Remover membro e banir de entrar novamente no futuro?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Removendo %1$s…"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Remover e banir participante"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Banir"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Não poderão voltar a entrar nesta sala, mesmo se forem convidados."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Tens a certeza que queres banir este participante?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"A banir %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Remover"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"Poderão entrar na sala novamente se convidados."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Tens a certeza que queres remover este membro?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Ver perfil"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Remover da sala"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Remover participante e proibir que entre no futuro?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"A remover %1$s…"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban">"Anular banimento da sala"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_action">"Anular banimento"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_description">"Poderão entrar novamente na sala se forem convidados"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_title">"Tens a certeza que queres anular o banimento deste utilizador?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unbanning_user">"A anular banimento de %1$s"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Eliminați și interziceți membrul"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Interzicere"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Nu se vor putea alătura din nou acestei camere dacă sunt invitați."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Sunteți sigur că doriți să interziceți acest membru?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Se interzice %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Vizualizare profil"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Înlăturați membrul"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Înlăturați membrul și interziceți-i să se alăture în viitor?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Se elimină %1$s"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Удалить и заблокировать участника"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Заблокировать"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Они не смогут снова присоединиться к этой комнате, если их пригласят."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Вы уверены, что хотите заблокировать этого участника?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Блокировка %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Удалить"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Вы действительно хотите удалить этого участника?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Посмотреть профиль"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Удалить участника из комнаты"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Удалить участника и запретить присоединяться в будущем?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Удаление %1$s…"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Odstrániť a zakázať člena"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Zakázať"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Nebudú sa môcť pripojiť k tejto miestnosti znova ani ak budú pozvaní."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Ste si istý, že chcete zakázať tohto člena?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Zakazuje sa %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Odstrániť"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"V prípade pozvania sa budú môcť znova pripojiť k tejto miestnosti."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Ste si istý, že chcete odstrániť tohto člena?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Zobraziť profil"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Odstrániť z miestnosti"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Odstrániť člena a zakázať vstup v budúcnosti?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Odstraňuje sa %1$s…"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban">"Zrušiť zákaz prístupu do miestnosti"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_action">"Zrušiť zákaz"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_description">"V prípade pozvania by sa mohli opäť pripojiť k miestnosti"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_title">"Naozaj chcete zrušiť zablokovanie tohto člena?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unbanning_user">"Zrušenie zákazu pre %1$s"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Ta bort och banna medlem"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Banna"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Denne kommer inte att kunna gå med i det här rummet igen om denne bjuds in."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Är du säker på att du vill banna den här medlemmen?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Bannar %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Ta bort"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"Denne kommer kunna gå med i rummet igen om denne bjuds in"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Är du säker på att du vill ta bort den här medlemmen?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Visa profil"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Ta bort från rummet"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Ta bort medlem och banna från att gå med i framtiden?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Tar bort %1$s …"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban">"Avbanna från rummet"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_action">"Avbanna"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_description">"De skulle kunna gå med i rummet igen om de blev inbjudna"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_title">"Är du säker på att du vill avbanna den här medlemmen?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unbanning_user">"Avbannar %1$s"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Üyeyi çıkar ve yasakla"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Yasakla"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Davet edilseler bile bu odaya tekrar katılamazlar."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Bu üyeyi yasaklamak istediğinize emin misiniz?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Yasaklanıyor %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Profili görüntüle"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Odadan çıkar"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Üyeyi çıkarın ve gelecekte katılmasını yasaklayın?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Kaldırılıyor %1$s…"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Вилучити й заблокувати учасника"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Заблокувати"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Він не зможе приєднатися до цієї кімнати знову, якщо його запросять."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Ви точно хочете заблокувати цього користувача?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Блокування %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Вилучити"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"Вони зможуть знову приєднатися до цієї кімнати, якщо їх запросять."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Ви дійсно хочете вилучити цього учасника?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Переглянути профіль"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Вилучити з кімнати"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Вилучити учасника та заборонити приєднання в майбутньому?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Вилучення %1$s…"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban">"Розблокувати в кімнаті"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_action">"Розблокувати"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_description">"Вони зможуть знову приєднатися до кімнати, якщо їх запросять"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_title">"Ви впевнені, що хочете розблокувати цього учасника?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unbanning_user">"Розблокування %1$s"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"کمرے سے محظور کریں"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"محظور کریں"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"اگر وہ مدعو کیا گیا تو وہ دوبارہ اس کمرے میں شامل نہیں ہوسکیں گے۔"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"کیا آپ کو یقین ہے کہ آپ اس رکن کو محظور کرنا چاہتے ہیں؟"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"%1$s کو محظور کر رہا ہے"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"نمایہ ملاحظہ کریں"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"کمرے سے ہٹائیں"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"رکن کو ہٹائیں اور مستقبل میں شمولیت پر پابندی لگائیں؟"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"%1$s کو ہٹا رہا ہے…"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"踢出並加入黑名單"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"加入黑名單"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"即使收到邀請,他們仍然無法加入聊天室。"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"您確定要將此成員加入黑名單?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"正在將 %1$s 加入黑名單"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"移除"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"若收到邀請,他們可以再次加入此聊天室。"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"您真的想要移除此成員嗎?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"查看個人檔案"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"踢出聊天室"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"移除成員並禁止未來再度加入?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"正在踢出 %1$s…"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"移除并封禁成员"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"封禁"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"即使受到邀请,他们也无法再次加入聊天室。"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"您确定要封禁该成员吗?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"封禁 %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"移除"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"如果受到邀请,他们可以重新加入聊天室。"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"您确定要移除此成员吗?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"查看个人资料"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"从聊天室移除"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"删除成员并禁止重新加入?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"正在移除 %1$s……"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Ban from room"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Ban"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"They won’t be able to join this room again if invited."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Are you sure you want to ban this member?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Banning %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Remove"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"They will be able to join this room again if invited."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Are you sure you want to remove this member?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"View profile"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Remove from room"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Remove member and ban from joining in the future?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Removing %1$s…"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban">"Unban from room"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_action">"Unban"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_description">"They would be able to join the room again if invited"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_title">"Are you sure you want to unban this member?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unbanning_user">"Unbanning %1$s"</string>
|
||||
</resources>
|
||||
|
|
@ -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<Unit> = Result.success(Unit),
|
||||
banUserResult: Result<Unit> = Result.success(Unit),
|
||||
unBanUserResult: Result<Unit> = 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<RoomMemberModerationState>.awaitState(): InternalRoomMemberModerationState {
|
||||
return awaitItem() as InternalRoomMemberModerationState
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `clicking on display profile action calls onSelectAction`() {
|
||||
val user = anAlice()
|
||||
val eventsRecorder = EventsRecorder<RoomMemberModerationEvents>(expectEvents = false)
|
||||
ensureCalledOnceWithTwoParams<ModerationAction, MatrixUser>(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<RoomMemberModerationEvents>(expectEvents = false)
|
||||
ensureCalledOnceWithTwoParams<ModerationAction, MatrixUser>(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<RoomMemberModerationEvents>(expectEvents = false)
|
||||
ensureCalledOnceWithTwoParams<ModerationAction, MatrixUser>(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<RoomMemberModerationEvents>(expectEvents = false)
|
||||
ensureCalledOnceWithTwoParams<ModerationAction, MatrixUser>(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<RoomMemberModerationEvents>()
|
||||
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<RoomMemberModerationEvents>()
|
||||
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<RoomMemberModerationEvents>()
|
||||
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<RoomMemberModerationEvents>()
|
||||
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<RoomMemberModerationEvents>()
|
||||
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<RoomMemberModerationEvents>()
|
||||
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<RoomMemberModerationEvents>(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 <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomMemberModerationView(
|
||||
state: InternalRoomMemberModerationState,
|
||||
onSelectAction: (ModerationAction, MatrixUser) -> Unit = EnsureNeverCalledWithTwoParams(),
|
||||
) {
|
||||
setContent {
|
||||
RoomMemberModerationView(
|
||||
state = state,
|
||||
onSelectAction = onSelectAction,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -294,15 +294,6 @@
|
|||
<string name="invite_friends_text">"Гэй, пагавары са мной у %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Паведаміць аб памылцы з дапамогай Rageshake"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Выдаліць і заблакіраваць удзельніка"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Заблакіраваць"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Яны не змогуць зноў далучыцца да гэтага пакоя, калі іх запросяць."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Вы ўпэўнены, што хочаце заблакіраваць гэтага карыстальніка?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Блакіроўка %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Прагляд профілю"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Выдаліць удзельніка з пакоя"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Выдаліць удзельніка і забараніць далучацца ў будучыні?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Выдаленне %1$s…"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Не ўдалося выбраць носьбіт, паўтарыце спробу."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Не атрымалася апрацаваць медыяфайл для загрузкі, паспрабуйце яшчэ раз."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Не атрымалася загрузіць медыяфайлы, паспрабуйце яшчэ раз."</string>
|
||||
|
|
|
|||
|
|
@ -348,23 +348,6 @@ Opravdu chcete pokračovat?"</string>
|
|||
<string name="invite_friends_text">"Ahoj, ozvi se mi na %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Zatřeste zařízením pro nahlášení chyby"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Odebrat a vykázat člena"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Vykázat"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Nebudou se moci znovu připojit k této místnosti, pokud budou pozváni."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Jste si jisti, že chcete vykázat tohoto člena?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Vykazování %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Odebrat"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"Budou moci znovu vstoupit do této místnosti, pokud budou pozváni."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Opravdu chcete tohoto člena odebrat?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Zobrazit profil"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Odebrat z místnosti"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Odebrat člena a zakázat mu připojení v budoucnu?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Odstraňování %1$s…"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban">"Zrušit vykázání z místnosti"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_action">"Zrušit vykázání"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_description">"Pokud by byli pozváni, mohli by se znovu připojit do místnosti"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_title">"Opravdu chcete zrušit vykázání tohoto člena?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unbanning_user">"Rušení vykázání %1$s"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Výběr média se nezdařil, zkuste to prosím znovu."</string>
|
||||
<string name="screen_media_upload_preview_caption_warning">"Titulky nemusí být viditelné pro lidi, kteří používají starší aplikace."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Nahrání média se nezdařilo, zkuste to prosím znovu."</string>
|
||||
|
|
|
|||
|
|
@ -363,23 +363,6 @@ Ydych chi\'n siŵr eich bod am barhau?"</string>
|
|||
<string name="invite_friends_text">"Hei, siaradwch â mi ar %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"Android %1$s"</string>
|
||||
<string name="preference_rageshake">"Rageshake i adrodd gwall"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Gwahardd o ystafell"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Atal"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Fyddan nhw ddim yn gallu ymuno â\'r ystafell hon eto os cân nhw wahoddiad."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Ydych chi\'n siŵr eich bod am wahardd yr aelod hwn?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Yn gwahardd %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Tynnu"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"Fyddan nhw yn gallu ymuno â\'r ystafell hon eto os cân nhw wahoddiad."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Ydych chi\'n siŵr eich bod am ddileu\'r aelod hwn?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Gweld proffil"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Tynnu o\'r ystafell"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Dileu aelod a\'u gwahardd rhag ymuno yn y dyfodol?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Wrthi\'n dileu %1$s…"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban">"Dad-wahardd o\'r ystafell"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_action">"Dad-wahardd"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_description">"Bydden nhw\'n gallu ymuno â\'r ystafell eto os fydd rhywun yn eu gwahodd"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_title">"Ydych chi\'n siŵr eich bod chi eisiau dadwahardd yr aelod hwn?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unbanning_user">"Dad-wahardd %1$s"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Wedi methu dewis cyfrwng, ceisiwch eto."</string>
|
||||
<string name="screen_media_upload_preview_caption_warning">"Efallai na fydd capsiynau yn weladwy i bobl sy\'n defnyddio apiau hŷn."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Wedi methu â phrosesu cyfryngau i\'w llwytho, ceisiwch eto."</string>
|
||||
|
|
|
|||
|
|
@ -343,23 +343,6 @@ Möchten Sie wirklich fortfahren?"</string>
|
|||
<string name="invite_friends_text">"Hey, sprich mit mir auf %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Schüttel heftig zum Melden von Fehlern"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Mitglied entfernen und sperren"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Sperren"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Sie können dem Raum nicht mehr beitreten, selbst wenn sie eingeladen werden."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Möchten Sie diesen Nutzer wirklich sperren?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"%1$s wird gesperrt."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Entfernen"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"Sie können diesen Raum wieder betreten, wenn sie eingeladen werden."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Möchten Sie dieses Mitglied wirklich entfernen?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Nutzerprofil anzeigen"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Mitglied entfernen"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Mitglied entfernen und für die Zukunft sperren?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"%1$s wird entfernt."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban">"Sperre für diesen Chatroom aufheben"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_action">"Sperre aufheben"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_description">"Sie könnten den Chatroom wieder betreten, wenn sie wieder eingeladen würden."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_title">"Möchten Sie die Sperre dieses Mitglieds wirklich aufheben?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unbanning_user">"Sperre für %1$s aufheben"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Medienauswahl fehlgeschlagen, bitte versuche es erneut."</string>
|
||||
<string name="screen_media_upload_preview_caption_warning">"Bildunterschriften sind für Nutzer älterer Apps möglicherweise nicht sichtbar."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuche es erneut."</string>
|
||||
|
|
|
|||
|
|
@ -343,18 +343,6 @@
|
|||
<string name="invite_friends_text">"Γεια, μίλα μου στην εφαρμογή %1$s :%2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Κούνησε δυνατά τη συσκευή σου για να αναφέρεις κάποιο σφάλμα"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Αφαίρεση και αποκλεισμός μέλους"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Αποκλεισμός"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Δεν θα μπορεί να συμμετέχει ξανά σε αυτό το δωμάτιο εάν προσκληθεί."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Θες σίγουρα να αποκλείσεις αυτό το μέλος;"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Αποκλεισμός %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Αφαίρεση"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"Θα μπορούν να συμμετάσχουν ξανά σε αυτό το δωμάτιο εάν προσκληθούν."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Είστε βέβαιοι ότι θέλετε να αφαιρέσετε αυτό το μέλος;"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Προβολή προφίλ"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Αφαίρεση από το δωμάτιο"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Αφαίρεση μέλους και απαγόρευση συμμετοχής στο μέλλον;"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Αφαίρεση %1$s…"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Αποτυχία επιλογής πολυμέσου, δοκίμασε ξανά."</string>
|
||||
<string name="screen_media_upload_preview_caption_warning">"Οι λεζάντες ενδέχεται να μην είναι ορατές σε άτομα που χρησιμοποιούν παλαιότερες εφαρμογές."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Αποτυχία μεταφόρτωσης μέσου, δοκίμασε ξανά."</string>
|
||||
|
|
|
|||
|
|
@ -320,15 +320,6 @@ Motivo: %1$s."</string>
|
|||
<string name="invite_friends_text">"Hola, puedes hablar conmigo en %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Agitar con fuerza para informar de un error"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Sacar y vetar a un miembro"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Vetar"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"No podrán volver a unirse a esta sala si son invitados."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"¿Estás seguro de que quieres vetar a este miembro?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Vetando a %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Ver perfil"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Sacar de la sala"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"¿Sacar al miembro y prohibirle unirse en el futuro?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Eliminando %1$s…"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Error al seleccionar archivos multimedia, por favor inténtalo de nuevo."</string>
|
||||
<string name="screen_media_upload_preview_caption_warning">"Es posible que las leyendas no sean visibles para las personas que usan aplicaciones más antiguas."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Error al procesar el contenido multimedia, por favor inténtalo de nuevo."</string>
|
||||
|
|
|
|||
|
|
@ -343,23 +343,6 @@ Kas sa oled kindel, et soovid jätkata?"</string>
|
|||
<string name="invite_friends_text">"Hei, suhtle minuga %1$s võrgus: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Veast teatamiseks raputa nutiseadet ägedalt"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Eemalda ja sea suhtluskeeld"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Sea suhtluskeeld"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Ta ei saa selle jututoaga liituda isegi kutse olemasolul."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Kas sa oled kindel, et soovid sellele kasutajale seada suhtluskeelu?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Seame kasutajale %1$s suhtluskeelu"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Eemalda"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"Uue kutse saamisel on tal võimalik selle jututoaga uuesti liituda."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Kas sa oled kindel, et soovid selle osaleja eemaldada?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Vaata profiili"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Eemalda kasutaja jututoast"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Kas eemaldama kasutaja ja seame talle tulevikuks suhtluskeelu?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Eemaldame kasutajat %1$s…"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban">"Eemalda suhtluskeeld jututoas"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_action">"Eemalda suhtluskeeld"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_description">"Ta võib kutse saamisel liituda jututoaga uuesti"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_title">"Kas oled kindel, et soovid selle liikme suhtluskeelu eemaldada?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unbanning_user">"Eemaldame suhtluskeelu kasutajalt %1$s"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Meediafaili valimine ei õnnestunud. Palun proovi uuesti."</string>
|
||||
<string name="screen_media_upload_preview_caption_warning">"Selgitused ja alapealkirjad ei pruugi olla nähtavad vanemate rakenduste kasutajatele."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Meediafaili töötlemine enne üleslaadimist ei õnnestunud. Palun proovi uuesti."</string>
|
||||
|
|
|
|||
|
|
@ -322,14 +322,6 @@ Arrazoia: %1$s."</string>
|
|||
<string name="invite_friends_rich_title">"🔐️ Zatoz nirekin %1$s(e)ra"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Astindu erroreen berri emateko"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Kendu kidea eta ezarri debekua"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Ezarri debekua"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Ziur kide honi debekua ezarri nahi diozula?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"%1$s(r)i debekua ezartzen"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Ikusi profila"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Kendu gelatik"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Kidea kendu eta etorkizunean sartzea debekatu?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"%1$s kentzen…"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Huts egin du multimedia aukeratzeak, saiatu berriro."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Huts egin du multimedia igotzeak, saiatu berriro."</string>
|
||||
<string name="screen_pinned_timeline_screen_title_empty">"Finkatutako mezuak"</string>
|
||||
|
|
|
|||
|
|
@ -310,18 +310,6 @@
|
|||
<string name="invite_friends_rich_title">"🔐️ پییوستن به من روی %1$s"</string>
|
||||
<string name="invite_friends_text">"درود. با من روی %1$s صحبت کن: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s اندروید"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"برداشت و تحریم عضو"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"تحریم"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"در صورت دعوت نمیتواند دوباره به اتاق بپیوندد."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"مطمئنید میخواهید این عضو را تحریم کنید؟"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"تحریم کردن %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"برداشتن"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"در صورت دعوت میتواند دوباره به اتاق بپیوندد."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"مطمئنید میخواهید این عضو را بردارید؟"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"دیدن نمایه"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"برداشتن از اتاق"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"برداشتن عضو و تحریم پیوستن در آینده؟"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"برداشتن %1$s…"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"گزینش رسانه شکست خورد. لطفاً دوباره تلاش کنید."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"پردازش رسانه برای بارگذاری شکست خورد. لطفاً دوباره تلاش کنید."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"بارگذاری رسانه شکست خورد. لطفاً دوباره تلاش کنید."</string>
|
||||
|
|
|
|||
|
|
@ -343,23 +343,6 @@ Haluatko varmasti jatkaa?"</string>
|
|||
<string name="invite_friends_text">"Hei, keskustele kanssani %1$s -sovelluksessa: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Raivostunut ravistaminen ilmoittaa virheestä"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Poista jäsen huoneesta ja anna porttikielto"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Anna porttikielto"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"He eivät voi enää liittyä tähän huoneeseen, jos heidät kutsutaan."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Haluatko varmasti antaa tälle jäsenelle porttikiellon?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Annetaan porttikieltoa käyttäjälle %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Poista"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"He voivat liittyä tähän huoneeseen uudelleen, jos heidät kutsutaan."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Haluatko varmasti poistaa tämän jäsenen?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Näytä profiili"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Poista huoneesta"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Poistetaanko jäsen huoneesta ja kielletäänkö heitä liittymästä tulevaisuudessa?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Poistetaan käyttäjää %1$s huoneesta…"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban">"Poista porttikielto huoneesta"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_action">"Poista porttikielto"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_description">"He voivat liittyä huoneeseen uudelleen, jos heidät kutsutaan"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_title">"Haluatko varmasti poistaa tämän jäsenen porttikiellon?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unbanning_user">"Poistetaan käyttäjän %1$s porttikieltoa"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Median valinta epäonnistui, yritä uudelleen."</string>
|
||||
<string name="screen_media_upload_preview_caption_warning">"Kuvatekstit eivät välttämättä näy ihmisille, jotka käyttävät vanhempia sovelluksia."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Median käsittely epäonnistui, yritä uudelleen."</string>
|
||||
|
|
|
|||
|
|
@ -343,23 +343,6 @@ Raison : %1$s."</string>
|
|||
<string name="invite_friends_text">"Salut, parle-moi sur %1$s : %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Rageshake pour signaler un problème"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Retirer et bannir ce membre"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Bannir"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Il ne pourra pas rejoindre le salon à nouveau, même si il est invité."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Êtes-vous certain de vouloir bannir ce membre ?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Bannissement de %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Retirer"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"Cet utilisateur pourra rejoindre le salon à nouveau si il est invité."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Voulez-vous vraiment supprimer ce membre ?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Voir le profil"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Retirer le membre du salon"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Retirer le membre et interdire l’adhésion à l’avenir ?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Enlever %1$s…"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban">"Débannir du salon"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_action">"Débannir"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_description">"L’utilisateur pourra à nouveau rejoindre le salon s’il est invité."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_title">"Êtes-vous sûr de vouloir débannir cet utilisateur?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unbanning_user">"Débannissement de %1$s"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Échec de la sélection du média, veuillez réessayer."</string>
|
||||
<string name="screen_media_upload_preview_caption_warning">"Les légendes peuvent ne pas être visibles pour les utilisateurs d’anciennes applications."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Échec du traitement des médias à télécharger, veuillez réessayer."</string>
|
||||
|
|
|
|||
|
|
@ -343,23 +343,6 @@ Biztos, hogy folytatja?"</string>
|
|||
<string name="invite_friends_text">"Beszélgessünk itt: %1$s, %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Az eszköz rázása a hibajelentéshez"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Eltávolítás és a tag kitiltása"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Kitiltás"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Többé nem csatlakozhat ehhez a szobához, akkor sem, ha meghívják."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Biztos, hogy kitiltja ezt a tagot?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"%1$s kitiltása"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Eltávolítás"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"Ehhez a szobához is csatlakozhat, ha meghívják."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Biztos, hogy eltávolítja ezt a tagot?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Profil megtekintése"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Eltávolítás a szobából"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Eltávolítja a tagot, és megtiltja a jövőbeni csatlakozást?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"%1$s eltávolítása…"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban">"Visszaengedés a szobába"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_action">"Kitiltás visszavonása"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_description">"Újra beléphetnek a szobába, ha meghívják őket."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_title">"Biztos, hogy feloldja a felhasználó kitiltását?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unbanning_user">"%1$s kitiltásának feloldása"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Nem sikerült kiválasztani a médiát, próbálja újra."</string>
|
||||
<string name="screen_media_upload_preview_caption_warning">"Előfordulhat, hogy a feliratok nem láthatók a régebbi alkalmazásokat használók számára."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Nem sikerült feldolgozni a feltöltendő médiát, próbálja újra."</string>
|
||||
|
|
|
|||
|
|
@ -307,15 +307,6 @@ Alasan: %1$s."</string>
|
|||
<string name="invite_friends_text">"Hai, bicaralah dengan saya di %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Rageshake untuk melaporkan kutu"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Keluarkan dan cekal anggota"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Cekal"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Mereka tidak akan dapat bergabung ke ruangan ini lagi jika diundang."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Apakah Anda yakin ingin mencekal anggota ini?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Mencekal %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Tampilkan profil"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Keluarkan dari ruangan"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Keluarkan pengguna dan cekal pengguna bergabung lagi di masa mendatang?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Mengeluarkan %1$s…"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Gagal memilih media, silakan coba lagi."</string>
|
||||
<string name="screen_media_upload_preview_caption_warning">"Keterangan mungkin tidak terlihat oleh orang yang menggunakan aplikasi lama."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Gagal memproses media untuk diunggah, silakan coba lagi."</string>
|
||||
|
|
|
|||
|
|
@ -330,15 +330,6 @@ Sei sicuro di voler continuare?"</string>
|
|||
<string name="invite_friends_text">"Ehi, parliamo su %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Scuoti per segnalare un problema"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Rimuovi ed escludi"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Escludi"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Non potrà entrare nuovamente in questa stanza se invitato."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Vuoi davvero escludere questo membro?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Esclusione di %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Visualizza profilo"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Rimuovi dalla stanza"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Rimuovere e vietare l\'accesso in futuro?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Rimozione di %1$s…"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Selezione del file multimediale fallita, riprova."</string>
|
||||
<string name="screen_media_upload_preview_caption_warning">"Le didascalie potrebbero non essere visibili agli utenti di app meno recenti."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Elaborazione del file multimediale da caricare fallita, riprova."</string>
|
||||
|
|
|
|||
|
|
@ -251,15 +251,6 @@
|
|||
<string name="invite_friends_text">"გაგიმარჯოს! მესაუბრე %1$s-ზე: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"შეცდომის შესატყობინებლად ტელეფონის შენჯღრევა"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"წევრის წაშლა და დაბლოკვა"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"დაბლოკვა"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"მოწვევის შემთხვევაში ამ ოთახში კვლავ გაწევრიანებას ვერ შეძლებენ."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"დარწმუნებული ხართ, რომ ამ წევრის დაბლოკვა გსურთ?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"%1$s-ს დაბლოკვა"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"პროფილის ნახვა"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"ოთახიდან გაგდება"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"გსურთ წევრის გაგდება და მომავალში გაწევრიანების აკრძალვა?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"%1$s-ს გაგდება…"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"მედიის შერჩევა ვერ მოხერხდა, გთხოვთ, სცადოთ ხელახლა."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"მედიის ატვირთვა ვერ მოხერხდა. გთხოვთ, სცადოთ ხელახლა."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"მედიის ატვირთვა ვერ მოხერხდა, გთხოვთ, სცადოთ ხელახლა."</string>
|
||||
|
|
|
|||
|
|
@ -343,23 +343,6 @@ Er du sikker på at du vil fortsette?"</string>
|
|||
<string name="invite_friends_text">"Hei, snakk med meg på %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Rageshake for å rapportere feil"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Fjern og utesteng medlem"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Utesteng"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"De vil ikke kunne bli med i dette rommet igjen hvis de blir invitert."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Er du sikker på at du vil utestenge dette medlemmet?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Utestenger %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Fjern"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"De vil kunne bli med i dette rommet igjen hvis de blir invitert."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Er du sikker på at du vil fjerne dette medlemmet?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Vis profil"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Fjern fra rommet"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Fjerne medlem og utestenge fra å bli med i fremtiden?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Fjerner %1$s…"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban">"Fjern utestengelsen fra rommet"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_action">"Opphev utestengelsen"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_description">"De vil kunne bli med i rommet igjen hvis de blir invitert"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_title">"Er du sikker på at du vil oppheve utestengelsen av dette medlemmet?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unbanning_user">"Oppheve utestengelsen av %1$s"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Kunne ikke velge medium, prøv igjen."</string>
|
||||
<string name="screen_media_upload_preview_caption_warning">"Teksting er kanskje ikke synlig for personer som bruker eldre apper."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Kunne ikke behandle medier for opplasting, vennligst prøv igjen."</string>
|
||||
|
|
|
|||
|
|
@ -296,15 +296,6 @@ Reden: %1$s."</string>
|
|||
<string name="invite_friends_text">"Hé, praat met me op %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Schudden om een bug te melden"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Lid verwijderen en verbannen"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Verbannen"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Ze kunnen niet meer toetreden tot deze kamer als ze worden uitgenodigd."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Weet je zeker dat je dit lid wilt verbannen?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"%1$s verbannen"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Profiel bekijken"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Verwijderen uit kamer"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Lid verwijderen en toekomstige deelname verbieden?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"%1$s wordt verwijderd…"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Het selecteren van media is mislukt. Probeer het opnieuw."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Het verwerken van media voor uploaden is mislukt. Probeer het opnieuw."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Het uploaden van media is mislukt. Probeer het opnieuw."</string>
|
||||
|
|
|
|||
|
|
@ -348,18 +348,6 @@ Czy na pewno chcesz kontynuować?"</string>
|
|||
<string name="invite_friends_text">"Hej, porozmawiajmy na %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Wstrząśnij gniewnie, aby zgłosić błąd"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Usuń i zbanuj członka"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Zbanuj"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Nie będą mogli ponownie dołączyć do tego pokoju, jeśli zostaną zaproszeni."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Czy na pewno chcesz zbanować tego członka?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Banowanie %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Usuń"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"Będą mogli ponownie dołączyć do pokoju, jeśli zostaną zaproszeni."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Czy na pewno chcesz usunąć tego członka?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Wyświetl profil"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Usuń z pokoju"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Usunąć członka i zablokować możliwość dołączenia w przyszłości?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Usuwanie %1$s…"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Nie udało się wybrać multimediów. Spróbuj ponownie."</string>
|
||||
<string name="screen_media_upload_preview_caption_warning">"Opis może być niedostępny dla osób korzystających ze starszej wersji aplikacji."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Przetwarzanie multimediów do przesłania nie powiodło się, spróbuj ponownie."</string>
|
||||
|
|
|
|||
|
|
@ -343,18 +343,6 @@ Você tem certeza de que deseja continuar?"</string>
|
|||
<string name="invite_friends_text">"Ei, fale comigo em %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Rageshake para relatar um bug"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Remover e banir membro"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Banir"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Eles não poderão entrar nesta sala novamente se forem convidados."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Tem certeza de que quer banir este membro?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Banindo %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Remover"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"Eles poderão entrar nesta sala novamente se forem convidados."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Tem certeza de que deseja remover este membro?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Ver perfil"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Remover da sala"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Remover membro e banir de entrar novamente no futuro?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Removendo %1$s…"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Falha ao selecionar a mídia, tente novamente."</string>
|
||||
<string name="screen_media_upload_preview_caption_warning">"As legendas podem não ser visíveis para pessoas que usam aplicativos mais antigos."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Falha ao processar mídia para upload. Tente novamente."</string>
|
||||
|
|
|
|||
|
|
@ -343,23 +343,6 @@ Tens a certeza de que queres continuar?"</string>
|
|||
<string name="invite_friends_text">"Alô! Fala comigo na %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Agita o dispositivo em fúria para comunicar um problema"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Remover e banir participante"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Banir"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Não poderão voltar a entrar nesta sala, mesmo se forem convidados."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Tens a certeza que queres banir este participante?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"A banir %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Remover"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"Poderão entrar na sala novamente se convidados."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Tens a certeza que queres remover este membro?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Ver perfil"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Remover da sala"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Remover participante e proibir que entre no futuro?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"A remover %1$s…"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban">"Anular banimento da sala"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_action">"Anular banimento"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_description">"Poderão entrar novamente na sala se forem convidados"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_title">"Tens a certeza que queres anular o banimento deste utilizador?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unbanning_user">"A anular banimento de %1$s"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Falha ao selecionar multimédia, por favor tente novamente."</string>
|
||||
<string name="screen_media_upload_preview_caption_warning">"As legendas poderão não ser visíveis em versões mais antigas da aplicação."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Falha ao processar multimédia para carregamento, por favor tente novamente."</string>
|
||||
|
|
|
|||
|
|
@ -299,15 +299,6 @@ Motiv:%1$s."</string>
|
|||
<string name="invite_friends_text">"Hei, vorbește cu mine pe %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Rageshake pentru a raporta erori"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Eliminați și interziceți membrul"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Interzicere"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Nu se vor putea alătura din nou acestei camere dacă sunt invitați."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Sunteți sigur că doriți să interziceți acest membru?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Se interzice %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Vizualizare profil"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Înlăturați membrul"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Înlăturați membrul și interziceți-i să se alăture în viitor?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Se elimină %1$s"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Selectarea fișierelor media a eșuat, încercați din nou."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Procesarea datelor media a eșuat, vă rugăm să încercați din nou."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Încărcarea fișierelor media a eșuat, încercați din nou."</string>
|
||||
|
|
|
|||
|
|
@ -348,17 +348,6 @@
|
|||
<string name="invite_friends_text">"Привет, поговори со мной по %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Встряхните устройство, чтобы сообщить об ошибке"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Удалить и заблокировать участника"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Заблокировать"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Они не смогут снова присоединиться к этой комнате, если их пригласят."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Вы уверены, что хотите заблокировать этого участника?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Блокировка %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Удалить"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Вы действительно хотите удалить этого участника?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Посмотреть профиль"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Удалить участника из комнаты"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Удалить участника и запретить присоединяться в будущем?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Удаление %1$s…"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Не удалось выбрать носитель, попробуйте еще раз."</string>
|
||||
<string name="screen_media_upload_preview_caption_warning">"Подпись может быть не видна пользователям старых приложений."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Не удалось обработать медиафайл для загрузки, попробуйте еще раз."</string>
|
||||
|
|
|
|||
|
|
@ -348,23 +348,6 @@ Naozaj chcete pokračovať?"</string>
|
|||
<string name="invite_friends_text">"Ahoj, porozprávajte sa so mnou na %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Zúrivo potriasť pre nahlásenie chyby"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban">"Odstrániť a zakázať člena"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_action">"Zakázať"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_description">"Nebudú sa môcť pripojiť k tejto miestnosti znova ani ak budú pozvaní."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_ban_member_confirmation_title">"Ste si istý, že chcete zakázať tohto člena?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_banning_user">"Zakazuje sa %1$s"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_action">"Odstrániť"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_description">"V prípade pozvania sa budú môcť znova pripojiť k tejto miestnosti."</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_kick_member_confirmation_title">"Ste si istý, že chcete odstrániť tohto člena?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_member_user_info">"Zobraziť profil"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove">"Odstrániť z miestnosti"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Odstrániť člena a zakázať vstup v budúcnosti?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Odstraňuje sa %1$s…"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban">"Zrušiť zákaz prístupu do miestnosti"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_action">"Zrušiť zákaz"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_description">"V prípade pozvania by sa mohli opäť pripojiť k miestnosti"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_title">"Naozaj chcete zrušiť zablokovanie tohto člena?"</string>
|
||||
<string name="screen_bottom_sheet_manage_room_member_unbanning_user">"Zrušenie zákazu pre %1$s"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Nepodarilo sa vybrať médium, skúste to prosím znova."</string>
|
||||
<string name="screen_media_upload_preview_caption_warning">"Titulky nemusia byť viditeľné pre ľudí používajúcich staršie aplikácie."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova."</string>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue