change (member moderation) : branch moderation on timeline

This commit is contained in:
ganfra 2025-05-13 11:39:19 +02:00
parent 58d9b12ab3
commit a09cc8de97
22 changed files with 169 additions and 95 deletions

View file

@ -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
}

View file

@ -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)

View file

@ -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) }
)
}

View file

@ -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
)

View file

@ -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,

View file

@ -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,

View file

@ -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) {

View file

@ -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,

View file

@ -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,

View file

@ -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() {

View file

@ -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,

View file

@ -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,