change (member moderation) : branch moderation on timeline
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -9,12 +9,15 @@ package io.element.android.features.messages.impl
|
||||
|
||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
|
||||
sealed interface MessagesEvents {
|
||||
data class HandleAction(val action: TimelineItemAction, val event: TimelineItem.Event) : MessagesEvents
|
||||
data class ToggleReaction(val emoji: String, val eventOrTransactionId: EventOrTransactionId) : MessagesEvents
|
||||
data class InviteDialogDismissed(val action: InviteDialogAction) : MessagesEvents
|
||||
data class OnUserClicked(val user: MatrixUser) : MessagesEvents
|
||||
data object Dismiss : MessagesEvents
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,9 @@ import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
||||
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
|
||||
import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.roommembermoderation.api.ModerationAction
|
||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents
|
||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationRenderer
|
||||
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
|
||||
import io.element.android.libraries.androidutils.system.openUrlInExternalApp
|
||||
import io.element.android.libraries.androidutils.system.toast
|
||||
@@ -76,7 +79,8 @@ class MessagesNode @AssistedInject constructor(
|
||||
private val timelineItemPresenterFactories: TimelineItemPresenterFactories,
|
||||
private val mediaPlayer: MediaPlayer,
|
||||
private val permalinkParser: PermalinkParser,
|
||||
private val knockRequestsBannerRenderer: KnockRequestsBannerRenderer
|
||||
private val knockRequestsBannerRenderer: KnockRequestsBannerRenderer,
|
||||
private val roomMemberModerationRenderer: RoomMemberModerationRenderer,
|
||||
) : Node(buildContext, plugins = plugins), MessagesNavigator {
|
||||
private val presenter = presenterFactory.create(
|
||||
navigator = this,
|
||||
@@ -257,6 +261,16 @@ class MessagesNode @AssistedInject constructor(
|
||||
},
|
||||
modifier = modifier,
|
||||
)
|
||||
roomMemberModerationRenderer.Render(
|
||||
state = state.roomMemberModerationState,
|
||||
onSelectAction = { action ->
|
||||
when (action) {
|
||||
is ModerationAction.DisplayProfile -> onUserDataClick(action.user.userId)
|
||||
else -> state.roomMemberModerationState.eventSink(RoomMemberModerationEvents.ProcessAction(action))
|
||||
}
|
||||
},
|
||||
modifier = Modifier,
|
||||
)
|
||||
|
||||
var focusedEventId by rememberSaveable {
|
||||
mutableStateOf(inputs.focusedEventId)
|
||||
|
||||
@@ -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,11 +37,14 @@ import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMe
|
||||
import io.element.android.features.roomcall.api.RoomCallState
|
||||
import io.element.android.features.roomcall.api.aStandByCallState
|
||||
import io.element.android.features.roomcall.api.anOngoingCallState
|
||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents
|
||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||
import io.element.android.libraries.textcomposer.model.aTextEditorStateRich
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
@@ -116,6 +119,7 @@ fun aMessagesState(
|
||||
roomCallState: RoomCallState = aStandByCallState(),
|
||||
pinnedMessagesBannerState: PinnedMessagesBannerState = aLoadedPinnedMessagesBannerState(),
|
||||
dmUserVerificationState: IdentityState? = null,
|
||||
roomMemberModerationState: RoomMemberModerationState = aRoomMemberModerationState(),
|
||||
eventSink: (MessagesEvents) -> Unit = {},
|
||||
) = MessagesState(
|
||||
roomId = RoomId("!id:domain"),
|
||||
@@ -143,9 +147,20 @@ fun aMessagesState(
|
||||
appName = "Element",
|
||||
pinnedMessagesBannerState = pinnedMessagesBannerState,
|
||||
dmUserVerificationState = dmUserVerificationState,
|
||||
roomMemberModerationState = roomMemberModerationState,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
fun aRoomMemberModerationState(
|
||||
canKick: Boolean = false,
|
||||
canBan: Boolean = false,
|
||||
|
||||
) = object : RoomMemberModerationState {
|
||||
override val canKick: Boolean = canKick
|
||||
override val canBan: Boolean = canBan
|
||||
override val eventSink: (RoomMemberModerationEvents) -> Unit = {}
|
||||
}
|
||||
|
||||
fun aUserEventPermissions(
|
||||
canRedactOwn: Boolean = false,
|
||||
canRedactOther: Boolean = false,
|
||||
|
||||
@@ -103,6 +103,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbar
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.textcomposer.model.TextEditorState
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.wysiwyg.link.Link
|
||||
@@ -208,7 +209,9 @@ fun MessagesView(
|
||||
.consumeWindowInsets(padding),
|
||||
onContentClick = ::onContentClick,
|
||||
onMessageLongClick = ::onMessageLongClick,
|
||||
onUserDataClick = { hidingKeyboard { onUserDataClick(it) } },
|
||||
onUserDataClick = { hidingKeyboard {
|
||||
state.eventSink(MessagesEvents.OnUserClicked(it))
|
||||
} },
|
||||
onLinkClick = { link, customTab ->
|
||||
if (customTab) {
|
||||
onLinkClick(link.url, true)
|
||||
@@ -293,7 +296,7 @@ private fun ReinviteDialog(state: MessagesState) {
|
||||
private fun MessagesViewContent(
|
||||
state: MessagesState,
|
||||
onContentClick: (TimelineItem.Event) -> Unit,
|
||||
onUserDataClick: (UserId) -> Unit,
|
||||
onUserDataClick: (MatrixUser) -> Unit,
|
||||
onLinkClick: (Link, Boolean) -> Unit,
|
||||
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
|
||||
onReactionLongClick: (key: String, TimelineItem.Event) -> Unit,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -49,6 +49,7 @@ import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.analytics.compose.LocalAnalyticsService
|
||||
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
||||
@@ -59,7 +60,7 @@ fun PinnedMessagesListView(
|
||||
state: PinnedMessagesListState,
|
||||
onBackClick: () -> Unit,
|
||||
onEventClick: (event: TimelineItem.Event) -> Unit,
|
||||
onUserDataClick: (UserId) -> Unit,
|
||||
onUserDataClick: (MatrixUser) -> Unit,
|
||||
onLinkClick: (Link) -> Unit,
|
||||
onLinkLongClick: (Link) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -115,7 +116,7 @@ private fun PinnedMessagesListTopBar(
|
||||
private fun PinnedMessagesListContent(
|
||||
state: PinnedMessagesListState,
|
||||
onEventClick: (event: TimelineItem.Event) -> Unit,
|
||||
onUserDataClick: (UserId) -> Unit,
|
||||
onUserDataClick: (MatrixUser) -> Unit,
|
||||
onLinkClick: (Link) -> Unit,
|
||||
onLinkLongClick: (Link) -> Unit,
|
||||
onErrorDismiss: () -> Unit,
|
||||
@@ -171,7 +172,7 @@ private fun PinnedMessagesListEmpty(
|
||||
private fun PinnedMessagesListLoaded(
|
||||
state: PinnedMessagesListState.Filled,
|
||||
onEventClick: (event: TimelineItem.Event) -> Unit,
|
||||
onUserDataClick: (UserId) -> Unit,
|
||||
onUserDataClick: (MatrixUser) -> Unit,
|
||||
onLinkClick: (Link) -> Unit,
|
||||
onLinkLongClick: (Link) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
|
||||
@@ -71,6 +71,7 @@ import io.element.android.libraries.designsystem.utils.animateScrollToItemCenter
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.testtags.TestTags
|
||||
import io.element.android.libraries.testtags.testTag
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
@@ -92,7 +93,7 @@ import kotlin.time.Duration.Companion.milliseconds
|
||||
fun TimelineView(
|
||||
state: TimelineState,
|
||||
timelineProtectionState: TimelineProtectionState,
|
||||
onUserDataClick: (UserId) -> Unit,
|
||||
onUserDataClick: (MatrixUser) -> Unit,
|
||||
onLinkClick: (Link) -> Unit,
|
||||
onContentClick: (TimelineItem.Event) -> Unit,
|
||||
onMessageLongClick: (TimelineItem.Event) -> Unit,
|
||||
|
||||
@@ -87,6 +87,10 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.getAvatarUrl
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.getDisplayName
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
|
||||
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToView
|
||||
import io.element.android.libraries.matrix.ui.messages.reply.eventId
|
||||
@@ -122,7 +126,7 @@ fun TimelineItemEventRow(
|
||||
onLongClick: () -> Unit,
|
||||
onLinkClick: (Link) -> Unit,
|
||||
onLinkLongClick: (Link) -> Unit,
|
||||
onUserDataClick: (UserId) -> Unit,
|
||||
onUserDataClick: (MatrixUser) -> Unit,
|
||||
inReplyToClick: (EventId) -> Unit,
|
||||
onReactionClick: (emoji: String, eventId: TimelineItem.Event) -> Unit,
|
||||
onReactionLongClick: (emoji: String, eventId: TimelineItem.Event) -> Unit,
|
||||
@@ -160,7 +164,12 @@ fun TimelineItemEventRow(
|
||||
}
|
||||
|
||||
fun onUserDataClick() {
|
||||
onUserDataClick(event.senderId)
|
||||
val sender = MatrixUser(
|
||||
userId = event.senderId,
|
||||
displayName = event.senderProfile.getDisplayName(),
|
||||
avatarUrl = event.senderProfile.getAvatarUrl(),
|
||||
)
|
||||
onUserDataClick(sender)
|
||||
}
|
||||
|
||||
fun inReplyToClick() {
|
||||
|
||||
@@ -35,6 +35,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.wysiwyg.link.Link
|
||||
|
||||
@Composable
|
||||
@@ -48,7 +49,7 @@ fun TimelineItemGroupedEventsRow(
|
||||
onClick: (TimelineItem.Event) -> Unit,
|
||||
onLongClick: (TimelineItem.Event) -> Unit,
|
||||
inReplyToClick: (EventId) -> Unit,
|
||||
onUserDataClick: (UserId) -> Unit,
|
||||
onUserDataClick: (MatrixUser) -> Unit,
|
||||
onLinkClick: (Link) -> Unit,
|
||||
onLinkLongClick: (Link) -> Unit,
|
||||
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
|
||||
@@ -117,7 +118,7 @@ private fun TimelineItemGroupedEventsRowContent(
|
||||
onClick: (TimelineItem.Event) -> Unit,
|
||||
onLongClick: (TimelineItem.Event) -> Unit,
|
||||
inReplyToClick: (EventId) -> Unit,
|
||||
onUserDataClick: (UserId) -> Unit,
|
||||
onUserDataClick: (MatrixUser) -> Unit,
|
||||
onLinkClick: (Link) -> Unit,
|
||||
onLinkLongClick: (Link) -> Unit,
|
||||
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
|
||||
|
||||
@@ -43,6 +43,7 @@ import io.element.android.libraries.designsystem.theme.LocalBuildMeta
|
||||
import io.element.android.libraries.designsystem.theme.highlightedMessageBackgroundColor
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.libraries.ui.utils.time.isTalkbackActive
|
||||
import io.element.android.wysiwyg.link.Link
|
||||
@@ -57,7 +58,7 @@ internal fun TimelineItemRow(
|
||||
isLastOutgoingMessage: Boolean,
|
||||
timelineProtectionState: TimelineProtectionState,
|
||||
focusedEventId: EventId?,
|
||||
onUserDataClick: (UserId) -> Unit,
|
||||
onUserDataClick: (MatrixUser) -> Unit,
|
||||
onLinkClick: (Link) -> Unit,
|
||||
onLinkLongClick: (Link) -> Unit,
|
||||
onContentClick: (TimelineItem.Event) -> Unit,
|
||||
|
||||
@@ -77,7 +77,7 @@ class RoomMemberListNode @AssistedInject constructor(
|
||||
state = state.moderationState,
|
||||
onSelectAction = { action ->
|
||||
when (action) {
|
||||
is ModerationAction.DisplayProfile -> openRoomMemberDetails(action.member.userId)
|
||||
is ModerationAction.DisplayProfile -> openRoomMemberDetails(action.user.userId)
|
||||
else -> state.moderationState.eventSink(RoomMemberModerationEvents.ProcessAction(action))
|
||||
}
|
||||
},
|
||||
|
||||
@@ -34,6 +34,7 @@ import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembersState
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||
import io.element.android.libraries.matrix.api.room.toMatrixUser
|
||||
import io.element.android.libraries.matrix.ui.room.canInviteAsState
|
||||
import io.element.android.libraries.matrix.ui.room.isDmAsState
|
||||
import io.element.android.libraries.matrix.ui.room.roomMemberIdentityStateChange
|
||||
@@ -166,9 +167,9 @@ class RoomMemberListPresenter @AssistedInject constructor(
|
||||
is RoomMemberListEvents.UpdateSearchQuery -> searchQuery = event.query
|
||||
is RoomMemberListEvents.RoomMemberSelected ->
|
||||
if (event.roomMember.membership == RoomMembershipState.BAN) {
|
||||
roomModerationState.eventSink(RoomMemberModerationEvents.ProcessAction(ModerationAction.UnbanUser(event.roomMember)))
|
||||
roomModerationState.eventSink(RoomMemberModerationEvents.ProcessAction(ModerationAction.UnbanUser(event.roomMember.toMatrixUser())))
|
||||
} else if (!isDm.value && (roomModerationState.canBan || roomModerationState.canKick)) {
|
||||
roomModerationState.eventSink(RoomMemberModerationEvents.RenderActions(event.roomMember))
|
||||
roomModerationState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(event.roomMember.toMatrixUser()))
|
||||
} else {
|
||||
navigator.openRoomMemberDetails(event.roomMember.userId)
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
|
||||
package io.element.android.features.roommembermoderation.api
|
||||
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
|
||||
interface RoomMemberModerationEvents {
|
||||
data class RenderActions(val roomMember: RoomMember) : RoomMemberModerationEvents
|
||||
data class ProcessAction(val action: ModerationAction): RoomMemberModerationEvents
|
||||
data class ShowActionsForUser(val user: MatrixUser) : RoomMemberModerationEvents
|
||||
data class ProcessAction(val action: ModerationAction) : RoomMemberModerationEvents
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
package io.element.android.features.roommembermoderation.api
|
||||
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
|
||||
interface RoomMemberModerationState {
|
||||
val canKick: Boolean
|
||||
@@ -16,8 +16,8 @@ interface RoomMemberModerationState {
|
||||
}
|
||||
|
||||
sealed interface ModerationAction {
|
||||
data class DisplayProfile(val member: RoomMember) : ModerationAction
|
||||
data class KickUser(val member: RoomMember) : ModerationAction
|
||||
data class BanUser(val member: RoomMember) : ModerationAction
|
||||
data class UnbanUser(val member: RoomMember) : ModerationAction
|
||||
data class DisplayProfile(val user: MatrixUser) : ModerationAction
|
||||
data class KickUser(val user: MatrixUser) : ModerationAction
|
||||
data class BanUser(val user: MatrixUser) : ModerationAction
|
||||
data class UnbanUser(val user: MatrixUser) : ModerationAction
|
||||
}
|
||||
|
||||
@@ -13,20 +13,19 @@ import io.element.android.features.roommembermoderation.api.RoomMemberModeration
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class InternalRoomMemberModerationState(
|
||||
override val canKick: Boolean,
|
||||
override val canBan: Boolean,
|
||||
val selectedRoomMember: AsyncData<RoomMember>,
|
||||
val selectedUser: MatrixUser?,
|
||||
val actions: ImmutableList<ModerationAction>,
|
||||
val kickUserAsyncAction: AsyncAction<Unit>,
|
||||
val banUserAsyncAction: AsyncAction<Unit>,
|
||||
val unbanUserAsyncAction: AsyncAction<Unit>,
|
||||
override val eventSink: (RoomMemberModerationEvents) -> Unit,
|
||||
) : RoomMemberModerationState {
|
||||
|
||||
val canOnlyDisplayProfile = actions.size == 1 && actions.first() is ModerationAction.DisplayProfile
|
||||
val canDisplayActions = actions.isNotEmpty() && !canOnlyDisplayProfile
|
||||
val canDisplayActions = actions.isNotEmpty()
|
||||
}
|
||||
|
||||
|
||||
@@ -20,13 +20,14 @@ import io.element.android.features.roommembermoderation.api.ModerationAction
|
||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents
|
||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runUpdatingState
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.toMatrixUser
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.room.canBanAsState
|
||||
import io.element.android.libraries.matrix.ui.room.canKickAsState
|
||||
import io.element.android.libraries.matrix.ui.room.userPowerLevelAsState
|
||||
@@ -45,7 +46,6 @@ class RoomMemberModerationPresenter @Inject constructor(
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : Presenter<RoomMemberModerationState> {
|
||||
private var selectedMember by mutableStateOf<AsyncData<RoomMember>>(AsyncData.Uninitialized)
|
||||
|
||||
@Composable
|
||||
override fun present(): RoomMemberModerationState {
|
||||
@@ -61,60 +61,68 @@ class RoomMemberModerationPresenter @Inject constructor(
|
||||
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<ModerationAction>()) }
|
||||
|
||||
fun handleEvent(event: RoomMemberModerationEvents) {
|
||||
when (event) {
|
||||
is RoomMemberModerationEvents.RenderActions -> {
|
||||
selectedMember = AsyncData.Success(event.roomMember)
|
||||
moderationActions.value = computeModerationActions(
|
||||
member = event.roomMember,
|
||||
canKick = canKick.value,
|
||||
canBan = canBan.value,
|
||||
currentUserMemberPowerLevel = currentUserMemberPowerLevel.value,
|
||||
)
|
||||
is RoomMemberModerationEvents.ShowActionsForUser -> {
|
||||
coroutineScope.launch {
|
||||
selectedUser = event.user
|
||||
moderationActions.value = persistentListOf(ModerationAction.DisplayProfile(event.user))
|
||||
room.getUpdatedMember(event.user.userId)
|
||||
.onSuccess {
|
||||
moderationActions.value = computeModerationActions(
|
||||
member = it,
|
||||
canKick = canKick.value,
|
||||
canBan = canBan.value,
|
||||
currentUserMemberPowerLevel = currentUserMemberPowerLevel.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is RoomMemberModerationEvents.ProcessAction -> {
|
||||
when(val action = event.action) {
|
||||
when (val action = event.action) {
|
||||
is ModerationAction.DisplayProfile -> Unit
|
||||
is ModerationAction.KickUser -> {
|
||||
selectedMember = AsyncData.Success(action.member)
|
||||
selectedUser = action.user
|
||||
kickUserAsyncAction.value = AsyncAction.ConfirmingNoParams
|
||||
}
|
||||
is ModerationAction.BanUser -> {
|
||||
selectedMember = AsyncData.Success(action.member)
|
||||
selectedUser = action.user
|
||||
banUserAsyncAction.value = AsyncAction.ConfirmingNoParams
|
||||
}
|
||||
is ModerationAction.UnbanUser -> {
|
||||
selectedMember = AsyncData.Success(action.member)
|
||||
selectedUser = action.user
|
||||
unbanUserAsyncAction.value = AsyncAction.ConfirmingNoParams
|
||||
}
|
||||
}
|
||||
}
|
||||
is InternalRoomMemberModerationEvents.DoKickUser -> {
|
||||
selectedMember.dataOrNull()?.let {
|
||||
selectedUser?.let {
|
||||
coroutineScope.kickUser(it.userId, event.reason, kickUserAsyncAction)
|
||||
}
|
||||
selectedMember = AsyncData.Uninitialized
|
||||
selectedUser = null
|
||||
}
|
||||
is InternalRoomMemberModerationEvents.DoBanUser -> {
|
||||
selectedMember.dataOrNull()?.let {
|
||||
selectedUser?.let {
|
||||
coroutineScope.banUser(it.userId, event.reason, banUserAsyncAction)
|
||||
}
|
||||
selectedMember = AsyncData.Uninitialized
|
||||
selectedUser = null
|
||||
}
|
||||
is InternalRoomMemberModerationEvents.Reset -> {
|
||||
selectedMember = AsyncData.Uninitialized
|
||||
selectedUser = null
|
||||
kickUserAsyncAction.value = AsyncAction.Uninitialized
|
||||
banUserAsyncAction.value = AsyncAction.Uninitialized
|
||||
unbanUserAsyncAction.value = AsyncAction.Uninitialized
|
||||
}
|
||||
is InternalRoomMemberModerationEvents.DoUnbanUser -> {
|
||||
selectedMember.dataOrNull()?.let {
|
||||
selectedUser?.let {
|
||||
coroutineScope.unbanUser(it.userId, unbanUserAsyncAction)
|
||||
}
|
||||
selectedMember = AsyncData.Uninitialized
|
||||
selectedUser = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -122,7 +130,7 @@ class RoomMemberModerationPresenter @Inject constructor(
|
||||
return InternalRoomMemberModerationState(
|
||||
canKick = canKick.value,
|
||||
canBan = canBan.value,
|
||||
selectedRoomMember = selectedMember,
|
||||
selectedUser = selectedUser,
|
||||
actions = moderationActions.value,
|
||||
kickUserAsyncAction = kickUserAsyncAction.value,
|
||||
banUserAsyncAction = banUserAsyncAction.value,
|
||||
@@ -137,13 +145,15 @@ class RoomMemberModerationPresenter @Inject constructor(
|
||||
canBan: Boolean,
|
||||
currentUserMemberPowerLevel: Long,
|
||||
): PersistentList<ModerationAction> {
|
||||
val memberAsUser = member.toMatrixUser()
|
||||
return buildList {
|
||||
add(ModerationAction.DisplayProfile(member))
|
||||
if (canKick && member.powerLevel < currentUserMemberPowerLevel) {
|
||||
add(ModerationAction.KickUser(member))
|
||||
add(ModerationAction.DisplayProfile(memberAsUser))
|
||||
val canModerateThisUser = member.powerLevel < currentUserMemberPowerLevel && member.membership.isActive()
|
||||
if (canKick && canModerateThisUser) {
|
||||
add(ModerationAction.KickUser(memberAsUser))
|
||||
}
|
||||
if (canBan && member.powerLevel < currentUserMemberPowerLevel) {
|
||||
add(ModerationAction.BanUser(member))
|
||||
if (canBan && canModerateThisUser) {
|
||||
add(ModerationAction.BanUser(memberAsUser))
|
||||
}
|
||||
}.toPersistentList()
|
||||
}
|
||||
@@ -194,4 +204,5 @@ class RoomMemberModerationPresenter @Inject constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,26 +15,28 @@ import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
import io.element.android.libraries.matrix.api.room.toMatrixUser
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
class RoomMemberModerationStateProvider : PreviewParameterProvider<InternalRoomMemberModerationState> {
|
||||
override val values: Sequence<InternalRoomMemberModerationState>
|
||||
get() = sequenceOf(
|
||||
aRoomMembersModerationState(
|
||||
selectedRoomMember = AsyncData.Success(anAlice()),
|
||||
selectedUser = anAlice(),
|
||||
actions = listOf(
|
||||
ModerationAction.DisplayProfile(anAlice()),
|
||||
),
|
||||
),
|
||||
aRoomMembersModerationState(
|
||||
selectedRoomMember = AsyncData.Success(anAlice()),
|
||||
selectedUser = anAlice(),
|
||||
actions = listOf(
|
||||
ModerationAction.DisplayProfile(anAlice()),
|
||||
ModerationAction.KickUser(anAlice()),
|
||||
),
|
||||
),
|
||||
aRoomMembersModerationState(
|
||||
selectedRoomMember = AsyncData.Success(anAlice()),
|
||||
selectedUser = anAlice(),
|
||||
actions = listOf(
|
||||
ModerationAction.DisplayProfile(anAlice()),
|
||||
ModerationAction.KickUser(anAlice()),
|
||||
@@ -42,41 +44,34 @@ class RoomMemberModerationStateProvider : PreviewParameterProvider<InternalRoomM
|
||||
),
|
||||
),
|
||||
aRoomMembersModerationState(
|
||||
selectedRoomMember = AsyncData.Success(anAlice()),
|
||||
selectedUser = anAlice(),
|
||||
kickUserAsyncAction = AsyncAction.ConfirmingNoParams,
|
||||
),
|
||||
aRoomMembersModerationState(
|
||||
selectedRoomMember = AsyncData.Success(anAlice()),
|
||||
selectedUser = anAlice(),
|
||||
kickUserAsyncAction = AsyncAction.Loading,
|
||||
),
|
||||
aRoomMembersModerationState(
|
||||
selectedRoomMember = AsyncData.Success(anAlice()),
|
||||
selectedUser = anAlice(),
|
||||
banUserAsyncAction = AsyncAction.ConfirmingNoParams,
|
||||
),
|
||||
aRoomMembersModerationState(
|
||||
selectedRoomMember = AsyncData.Success(anAlice()),
|
||||
selectedUser = anAlice(),
|
||||
banUserAsyncAction = AsyncAction.Loading,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun anAlice() = RoomMember(
|
||||
fun anAlice() = MatrixUser(
|
||||
UserId(value = "@alice:server.org"),
|
||||
displayName = "Alice",
|
||||
avatarUrl = null,
|
||||
role = RoomMember.Role.forPowerLevel(100L),
|
||||
membership = RoomMembershipState.JOIN,
|
||||
isNameAmbiguous = false,
|
||||
powerLevel = 100L,
|
||||
normalizedPowerLevel = 100L,
|
||||
isIgnored = false,
|
||||
membershipChangeReason = null,
|
||||
)
|
||||
|
||||
fun aRoomMembersModerationState(
|
||||
canKick: Boolean = false,
|
||||
canBan: Boolean = false,
|
||||
selectedRoomMember: AsyncData<RoomMember> = AsyncData.Uninitialized,
|
||||
selectedUser: MatrixUser? = null,
|
||||
actions: List<ModerationAction> = emptyList(),
|
||||
kickUserAsyncAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
banUserAsyncAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
@@ -85,7 +80,7 @@ fun aRoomMembersModerationState(
|
||||
) = InternalRoomMemberModerationState(
|
||||
canKick = canKick,
|
||||
canBan = canBan,
|
||||
selectedRoomMember = selectedRoomMember,
|
||||
selectedUser = selectedUser,
|
||||
actions = actions.toPersistentList(),
|
||||
kickUserAsyncAction = kickUserAsyncAction,
|
||||
banUserAsyncAction = banUserAsyncAction,
|
||||
|
||||
@@ -20,9 +20,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -49,9 +47,9 @@ import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||
import io.element.android.libraries.designsystem.theme.components.ListItemStyle
|
||||
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.getBestName
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
import io.element.android.libraries.matrix.ui.model.getBestName
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -63,23 +61,27 @@ fun RoomMemberModerationView(
|
||||
onSelectAction: (ModerationAction) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val selectedRoomMember = state.selectedRoomMember.dataOrNull()
|
||||
Box(modifier = modifier) {
|
||||
if (selectedRoomMember != null && state.canDisplayActions) {
|
||||
val selectedUser = state.selectedUser
|
||||
if (selectedUser != null && state.canDisplayActions) {
|
||||
RoomMemberActionsBottomSheet(
|
||||
roomMember = selectedRoomMember,
|
||||
user = selectedUser,
|
||||
actions = state.actions,
|
||||
onSelectAction = onSelectAction,
|
||||
onDismiss = { state.eventSink(InternalRoomMemberModerationEvents.Reset) },
|
||||
)
|
||||
}
|
||||
val onSelectAction by rememberUpdatedState(onSelectAction)
|
||||
LaunchedEffect(state.canOnlyDisplayProfile) {
|
||||
if (state.canOnlyDisplayProfile) {
|
||||
onSelectAction(state.actions.first())
|
||||
}
|
||||
}
|
||||
RoomMemberAsyncActions(state = state)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RoomMemberAsyncActions(
|
||||
state: InternalRoomMemberModerationState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(modifier = modifier) {
|
||||
val selectedUser = state.selectedUser
|
||||
val asyncIndicatorState = rememberAsyncIndicatorState()
|
||||
AsyncIndicatorHost(modifier = Modifier.statusBarsPadding(), state = asyncIndicatorState)
|
||||
|
||||
@@ -100,7 +102,7 @@ fun RoomMemberModerationView(
|
||||
}
|
||||
is AsyncAction.Loading -> {
|
||||
LaunchedEffect(action) {
|
||||
val userDisplayName = selectedRoomMember?.getBestName().orEmpty()
|
||||
val userDisplayName = selectedUser?.getBestName().orEmpty()
|
||||
asyncIndicatorState.enqueue {
|
||||
AsyncIndicator.Loading(text = stringResource(R.string.screen_bottom_sheet_manage_room_member_removing_user, userDisplayName))
|
||||
}
|
||||
@@ -139,7 +141,7 @@ fun RoomMemberModerationView(
|
||||
}
|
||||
is AsyncAction.Loading -> {
|
||||
LaunchedEffect(action) {
|
||||
val userDisplayName = selectedRoomMember?.getBestName().orEmpty()
|
||||
val userDisplayName = selectedUser?.getBestName().orEmpty()
|
||||
asyncIndicatorState.enqueue {
|
||||
AsyncIndicator.Loading(text = stringResource(R.string.screen_bottom_sheet_manage_room_member_banning_user, userDisplayName))
|
||||
}
|
||||
@@ -167,7 +169,7 @@ fun RoomMemberModerationView(
|
||||
content = stringResource(R.string.screen_room_member_list_manage_member_unban_message),
|
||||
submitText = stringResource(R.string.screen_room_member_list_manage_member_unban_action),
|
||||
onSubmitClick = {
|
||||
val userDisplayName = selectedRoomMember?.getBestName().orEmpty()
|
||||
val userDisplayName = selectedUser?.getBestName().orEmpty()
|
||||
asyncIndicatorState.enqueue {
|
||||
AsyncIndicator.Loading(text = stringResource(R.string.screen_room_member_list_unbanning_user, userDisplayName))
|
||||
}
|
||||
@@ -198,7 +200,7 @@ fun RoomMemberModerationView(
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun RoomMemberActionsBottomSheet(
|
||||
roomMember: RoomMember,
|
||||
user: MatrixUser,
|
||||
actions: ImmutableList<ModerationAction>,
|
||||
onSelectAction: (ModerationAction) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
@@ -219,12 +221,12 @@ private fun RoomMemberActionsBottomSheet(
|
||||
modifier = Modifier.padding(vertical = 16.dp)
|
||||
) {
|
||||
Avatar(
|
||||
avatarData = roomMember.getAvatarData(size = AvatarSize.RoomListManageUser),
|
||||
avatarData = user.getAvatarData(size = AvatarSize.RoomListManageUser),
|
||||
modifier = Modifier
|
||||
.padding(bottom = 28.dp)
|
||||
.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
roomMember.displayName?.let {
|
||||
user.displayName?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = ElementTheme.typography.fontHeadingLgBold,
|
||||
@@ -237,7 +239,7 @@ private fun RoomMemberActionsBottomSheet(
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = roomMember.userId.toString(),
|
||||
text = user.userId.toString(),
|
||||
style = ElementTheme.typography.fontBodyLgRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
maxLines = 1,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user