diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index cf8f73f47a..5789afab57 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -67,6 +67,7 @@ dependencies { implementation(libs.telephoto.zoomableimage) implementation(libs.matrix.emojibase.bindings) implementation(projects.features.knockrequests.api) + implementation(projects.features.roommembermoderation.api) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesEvents.kt index 8d4d597502..f299deaf0e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesEvents.kt @@ -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 } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt index d2d4b1c705..5f2546dd50 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt @@ -39,6 +39,9 @@ import io.element.android.features.messages.impl.timeline.TimelinePresenter import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.roommembermoderation.api.ModerationAction +import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationRenderer import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab import io.element.android.libraries.androidutils.system.openUrlInExternalApp import io.element.android.libraries.androidutils.system.toast @@ -76,7 +79,8 @@ class MessagesNode @AssistedInject constructor( private val timelineItemPresenterFactories: TimelineItemPresenterFactories, private val mediaPlayer: MediaPlayer, private val permalinkParser: PermalinkParser, - private val knockRequestsBannerRenderer: KnockRequestsBannerRenderer + private val knockRequestsBannerRenderer: KnockRequestsBannerRenderer, + private val roomMemberModerationRenderer: RoomMemberModerationRenderer, ) : Node(buildContext, plugins = plugins), MessagesNavigator { private val presenter = presenterFactory.create( navigator = this, @@ -257,6 +261,16 @@ class MessagesNode @AssistedInject constructor( }, modifier = modifier, ) + roomMemberModerationRenderer.Render( + state = state.roomMemberModerationState, + onSelectAction = { action -> + 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) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index 8bf5d330d7..6be5646649 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -50,6 +50,8 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState import io.element.android.features.roomcall.api.RoomCallState +import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.androidutils.clipboard.ClipboardHelper import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter @@ -103,6 +105,7 @@ class MessagesPresenter @AssistedInject constructor( private val readReceiptBottomSheetPresenter: Presenter, private val pinnedMessagesBannerPresenter: Presenter, private val roomCallStatePresenter: Presenter, + private val roomMemberModerationPresenter: Presenter, private val syncService: SyncService, private val snackbarDispatcher: SnackbarDispatcher, private val dispatchers: CoroutineDispatchers, @@ -143,7 +146,7 @@ class MessagesPresenter @AssistedInject constructor( val readReceiptBottomSheetState = readReceiptBottomSheetPresenter.present() val pinnedMessagesBannerState = pinnedMessagesBannerPresenter.present() val roomCallState = roomCallStatePresenter.present() - + val roomMemberModerationState = roomMemberModerationPresenter.present() val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val userEventPermissions by userEventPermissions(syncUpdateFlow.value) @@ -233,6 +236,9 @@ class MessagesPresenter @AssistedInject constructor( } } is MessagesEvents.Dismiss -> actionListState.eventSink(ActionListEvents.Clear) + is MessagesEvents.OnUserClicked -> { + roomMemberModerationState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(event.user)) + } } } @@ -262,6 +268,7 @@ class MessagesPresenter @AssistedInject constructor( roomCallState = roomCallState, pinnedMessagesBannerState = pinnedMessagesBannerState, dmUserVerificationState = dmUserVerificationState, + roomMemberModerationState = roomMemberModerationState, eventSink = { handleEvents(it) } ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt index 2a6889be41..8ba2cbd039 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt @@ -20,6 +20,7 @@ import io.element.android.features.messages.impl.timeline.components.receipt.bot import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState import io.element.android.features.roomcall.api.RoomCallState +import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage @@ -54,5 +55,6 @@ data class MessagesState( val appName: String, val pinnedMessagesBannerState: PinnedMessagesBannerState, val dmUserVerificationState: IdentityState?, + val roomMemberModerationState: RoomMemberModerationState, val eventSink: (MessagesEvents) -> Unit ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index 12c6d607b0..debee82b3a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -37,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, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 948b8c3cb3..262698894a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -103,6 +103,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbar import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.encryption.identity.IdentityState +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.textcomposer.model.TextEditorState import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.wysiwyg.link.Link @@ -208,7 +209,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, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt index 9827698f99..2dbf76bc8d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt @@ -33,6 +33,7 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.ui.strings.CommonStrings @ContributesNode(RoomScope::class) @@ -63,8 +64,8 @@ class PinnedMessagesListNode @AssistedInject constructor( return callbacks.forEach { it.onEventClick(event) } } - private fun onUserDataClick(userId: UserId) { - callbacks.forEach { it.onUserDataClick(userId) } + private fun onUserDataClick(user: MatrixUser) { + callbacks.forEach { it.onUserDataClick(user.userId) } } private fun onLinkClick(context: Context, url: String) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt index c87e4c0ffd..e0356f1670 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt @@ -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, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index c64eec9bee..b14ff51ce2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -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, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt index f3b890cb18..e38d212041 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt @@ -87,6 +87,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() { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt index 8f868370b3..4e55d6c3ff 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt @@ -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, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt index 88b19c43fe..136cdcbfed 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt @@ -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, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt index 05a35fad88..fd9dc1cd51 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt @@ -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)) } }, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt index 07688387e3..0066b80231 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt @@ -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) } diff --git a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationEvents.kt b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationEvents.kt index 265a59a125..277b4275d6 100644 --- a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationEvents.kt +++ b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationEvents.kt @@ -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 } diff --git a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationState.kt b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationState.kt index 410b8d6423..b31857b594 100644 --- a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationState.kt +++ b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationState.kt @@ -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 } diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationState.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationState.kt index e6258f2003..7e0c27a62d 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationState.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationState.kt @@ -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, + val selectedUser: MatrixUser?, val actions: ImmutableList, val kickUserAsyncAction: AsyncAction, val banUserAsyncAction: AsyncAction, val unbanUserAsyncAction: AsyncAction, 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() } diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt index 5f7fa80c60..fb9199ea54 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt @@ -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 { - private var selectedMember by mutableStateOf>(AsyncData.Uninitialized) @Composable override fun present(): RoomMemberModerationState { @@ -61,60 +61,68 @@ class RoomMemberModerationPresenter @Inject constructor( remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction) } val unbanUserAsyncAction = remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction) } - + var selectedUser by remember { + mutableStateOf(null) + } val moderationActions = remember { mutableStateOf(persistentListOf()) } fun handleEvent(event: RoomMemberModerationEvents) { when (event) { - is RoomMemberModerationEvents.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 { + 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( } } } + } diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationStateProvider.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationStateProvider.kt index b705e5217d..9d52eec503 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationStateProvider.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationStateProvider.kt @@ -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 { override val values: Sequence 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 = AsyncData.Uninitialized, + selectedUser: MatrixUser? = null, actions: List = emptyList(), kickUserAsyncAction: AsyncAction = AsyncAction.Uninitialized, banUserAsyncAction: AsyncAction = AsyncAction.Uninitialized, @@ -85,7 +80,7 @@ fun aRoomMembersModerationState( ) = InternalRoomMemberModerationState( canKick = canKick, canBan = canBan, - selectedRoomMember = selectedRoomMember, + selectedUser = selectedUser, actions = actions.toPersistentList(), kickUserAsyncAction = kickUserAsyncAction, banUserAsyncAction = banUserAsyncAction, diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt index ff949fde59..31b474057d 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt @@ -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, 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, diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/ProfileTimelineDetails.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/ProfileTimelineDetails.kt index ff1f9351f9..46ab482ad5 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/ProfileTimelineDetails.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/ProfileTimelineDetails.kt @@ -44,6 +44,13 @@ fun ProfileTimelineDetails.getDisambiguatedDisplayName(userId: UserId): String { } } +fun ProfileTimelineDetails.getDisplayName(): String? { + return when (this) { + is ProfileTimelineDetails.Ready -> displayName + else -> null + } +} + fun ProfileTimelineDetails.getAvatarUrl(): String? { return when (this) { is ProfileTimelineDetails.Ready -> avatarUrl