Skip to content

Change : RoomMember moderation #4779

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Jun 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions features/messages/impl/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -233,6 +236,9 @@ class MessagesPresenter @AssistedInject constructor(
}
}
is MessagesEvents.Dismiss -> actionListState.eventSink(ActionListEvents.Clear)
is MessagesEvents.OnUserClicked -> {
roomMemberModerationState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(event.user))
}
}
}

Expand Down Expand Up @@ -262,6 +268,7 @@ class MessagesPresenter @AssistedInject constructor(
roomCallState = roomCallState,
pinnedMessagesBannerState = pinnedMessagesBannerState,
dmUserVerificationState = dmUserVerificationState,
roomMemberModerationState = roomMemberModerationState,
eventSink = { handleEvents(it) }
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -54,5 +55,6 @@ data class MessagesState(
val appName: String,
val pinnedMessagesBannerState: PinnedMessagesBannerState,
val dmUserVerificationState: IdentityState?,
val roomMemberModerationState: RoomMemberModerationState,
val eventSink: (MessagesEvents) -> Unit
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1188,6 +1189,9 @@ class MessagesPresenterTest {
textEditorState = aTextEditorStateMarkdown(initialText = "", initialFocus = false)
)
},
roomMemberModerationPresenter: Presenter<RoomMemberModerationState> = Presenter {
aRoomMemberModerationState()
},
encryptionService: FakeEncryptionService = FakeEncryptionService(),
actionListEventSink: (ActionListEvents) -> Unit = {},
): MessagesPresenter {
Expand All @@ -1205,6 +1209,7 @@ class MessagesPresenterTest {
linkPresenter = { aLinkState() },
pinnedMessagesBannerPresenter = { aLoadedPinnedMessagesBannerState() },
roomCallStatePresenter = { aStandByCallState() },
roomMemberModerationPresenter = roomMemberModerationPresenter,
syncService = FakeSyncService(),
snackbarDispatcher = SnackbarDispatcher(),
navigator = navigator,
Expand Down
Loading
Loading