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