Timeline Action : refactor how it's computed and align with iOS

This commit is contained in:
ganfra 2024-07-26 18:20:03 +02:00
parent c59cfd707c
commit 3adc6d0be8
21 changed files with 323 additions and 296 deletions

View file

@ -20,6 +20,7 @@ import android.os.Build
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -131,10 +132,9 @@ class MessagesPresenter @AssistedInject constructor(
val readReceiptBottomSheetState = readReceiptBottomSheetPresenter.present() val readReceiptBottomSheetState = readReceiptBottomSheetPresenter.present()
val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
val userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value)
val userHasPermissionToRedactOwn by room.canRedactOwnAsState(updateKey = syncUpdateFlow.value) val userEventPermissions by userEventPermissions(syncUpdateFlow.value)
val userHasPermissionToRedactOther by room.canRedactOtherAsState(updateKey = syncUpdateFlow.value)
val userHasPermissionToSendReaction by room.canSendMessageAsState(type = MessageEventType.REACTION, updateKey = syncUpdateFlow.value)
val roomName: AsyncData<String> by remember { val roomName: AsyncData<String> by remember {
derivedStateOf { roomInfo?.name?.let { AsyncData.Success(it) } ?: AsyncData.Uninitialized } derivedStateOf { roomInfo?.name?.let { AsyncData.Success(it) } ?: AsyncData.Uninitialized }
} }
@ -211,11 +211,8 @@ class MessagesPresenter @AssistedInject constructor(
roomName = roomName, roomName = roomName,
roomAvatar = roomAvatar, roomAvatar = roomAvatar,
heroes = heroes, heroes = heroes,
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
userHasPermissionToRedactOwn = userHasPermissionToRedactOwn,
userHasPermissionToRedactOther = userHasPermissionToRedactOther,
userHasPermissionToSendReaction = userHasPermissionToSendReaction,
composerState = composerState, composerState = composerState,
userEventPermissions = userEventPermissions,
voiceMessageComposerState = voiceMessageComposerState, voiceMessageComposerState = voiceMessageComposerState,
timelineState = timelineState, timelineState = timelineState,
typingNotificationState = typingNotificationState, typingNotificationState = typingNotificationState,
@ -235,6 +232,24 @@ class MessagesPresenter @AssistedInject constructor(
) )
} }
@Composable
private fun userEventPermissions(updateKey: Long): State<UserEventPermissions> {
val userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = updateKey)
val userHasPermissionToRedactOwn by room.canRedactOwnAsState(updateKey = updateKey)
val userHasPermissionToRedactOther by room.canRedactOtherAsState(updateKey = updateKey)
val userHasPermissionToSendReaction by room.canSendMessageAsState(type = MessageEventType.REACTION, updateKey = updateKey)
return remember {
derivedStateOf {
UserEventPermissions(
canSendMessage = userHasPermissionToSendMessage,
canRedactOwn = userHasPermissionToRedactOwn,
canRedactOther = userHasPermissionToRedactOther,
canSendReaction = userHasPermissionToSendReaction,
)
}
}
}
private fun MatrixRoomInfo.avatarData(): AvatarData { private fun MatrixRoomInfo.avatarData(): AvatarData {
return AvatarData( return AvatarData(
id = id.value, id = id.value,

View file

@ -37,10 +37,7 @@ data class MessagesState(
val roomName: AsyncData<String>, val roomName: AsyncData<String>,
val roomAvatar: AsyncData<AvatarData>, val roomAvatar: AsyncData<AvatarData>,
val heroes: ImmutableList<AvatarData>, val heroes: ImmutableList<AvatarData>,
val userHasPermissionToSendMessage: Boolean, val userEventPermissions: UserEventPermissions,
val userHasPermissionToRedactOwn: Boolean,
val userHasPermissionToRedactOther: Boolean,
val userHasPermissionToSendReaction: Boolean,
val composerState: MessageComposerState, val composerState: MessageComposerState,
val voiceMessageComposerState: VoiceMessageComposerState, val voiceMessageComposerState: VoiceMessageComposerState,
val timelineState: TimelineState, val timelineState: TimelineState,

View file

@ -53,7 +53,7 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
aMessagesState(), aMessagesState(),
aMessagesState(hasNetworkConnection = false), aMessagesState(hasNetworkConnection = false),
aMessagesState(composerState = aMessageComposerState(showAttachmentSourcePicker = true)), aMessagesState(composerState = aMessageComposerState(showAttachmentSourcePicker = true)),
aMessagesState(userHasPermissionToSendMessage = false), aMessagesState(userEventPermissions = aUserEventPermissions(canSendMessage = false)),
aMessagesState(showReinvitePrompt = true), aMessagesState(showReinvitePrompt = true),
aMessagesState( aMessagesState(
roomName = AsyncData.Uninitialized, roomName = AsyncData.Uninitialized,
@ -93,10 +93,7 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
fun aMessagesState( fun aMessagesState(
roomName: AsyncData<String> = AsyncData.Success("Room name"), roomName: AsyncData<String> = AsyncData.Success("Room name"),
roomAvatar: AsyncData<AvatarData> = AsyncData.Success(AvatarData("!id:domain", "Room name", size = AvatarSize.TimelineRoom)), roomAvatar: AsyncData<AvatarData> = AsyncData.Success(AvatarData("!id:domain", "Room name", size = AvatarSize.TimelineRoom)),
userHasPermissionToSendMessage: Boolean = true, userEventPermissions: UserEventPermissions = aUserEventPermissions(),
userHasPermissionToRedactOwn: Boolean = false,
userHasPermissionToRedactOther: Boolean = false,
userHasPermissionToSendReaction: Boolean = true,
composerState: MessageComposerState = aMessageComposerState( composerState: MessageComposerState = aMessageComposerState(
textEditorState = TextEditorState.Rich(aRichTextEditorState(initialText = "Hello", initialFocus = true)), textEditorState = TextEditorState.Rich(aRichTextEditorState(initialText = "Hello", initialFocus = true)),
isFullScreen = false, isFullScreen = false,
@ -122,10 +119,7 @@ fun aMessagesState(
roomName = roomName, roomName = roomName,
roomAvatar = roomAvatar, roomAvatar = roomAvatar,
heroes = persistentListOf(), heroes = persistentListOf(),
userHasPermissionToSendMessage = userHasPermissionToSendMessage, userEventPermissions = userEventPermissions,
userHasPermissionToRedactOwn = userHasPermissionToRedactOwn,
userHasPermissionToRedactOther = userHasPermissionToRedactOther,
userHasPermissionToSendReaction = userHasPermissionToSendReaction,
composerState = composerState, composerState = composerState,
voiceMessageComposerState = voiceMessageComposerState, voiceMessageComposerState = voiceMessageComposerState,
typingNotificationState = aTypingNotificationState(), typingNotificationState = aTypingNotificationState(),
@ -145,6 +139,18 @@ fun aMessagesState(
eventSink = eventSink, eventSink = eventSink,
) )
fun aUserEventPermissions(
canRedactOwn: Boolean = false,
canRedactOther: Boolean = false,
canSendMessage: Boolean = true,
canSendReaction: Boolean = true,
) = UserEventPermissions(
canRedactOwn = canRedactOwn,
canRedactOther = canRedactOther,
canSendMessage = canSendMessage,
canSendReaction = canSendReaction,
)
fun aReactionSummaryState( fun aReactionSummaryState(
target: ReactionSummaryState.Summary? = null, target: ReactionSummaryState.Summary? = null,
eventSink: (ReactionSummaryEvents) -> Unit = {} eventSink: (ReactionSummaryEvents) -> Unit = {}

View file

@ -154,10 +154,7 @@ fun MessagesView(
state.actionListState.eventSink( state.actionListState.eventSink(
ActionListEvents.ComputeForMessage( ActionListEvents.ComputeForMessage(
event = event, event = event,
canRedactOwn = state.userHasPermissionToRedactOwn, userEventPermissions = state.userEventPermissions,
canRedactOther = state.userHasPermissionToRedactOther,
canSendMessage = state.userHasPermissionToSendMessage,
canSendReaction = state.userHasPermissionToSendReaction,
) )
) )
} }
@ -408,7 +405,7 @@ private fun MessagesViewComposerBottomSheetContents(
subcomposing: Boolean, subcomposing: Boolean,
state: MessagesState, state: MessagesState,
) { ) {
if (state.userHasPermissionToSendMessage) { if (state.userEventPermissions.canSendMessage) {
Column(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.fillMaxWidth()) {
MentionSuggestionsPickerView( MentionSuggestionsPickerView(
modifier = Modifier modifier = Modifier

View file

@ -0,0 +1,28 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl
/**
* Represents the permissions a user has in a room.
* It's dependent of the user's power level in the room.
*/
data class UserEventPermissions(
val canRedactOwn: Boolean,
val canRedactOther: Boolean,
val canSendMessage: Boolean,
val canSendReaction: Boolean,
)

View file

@ -16,15 +16,13 @@
package io.element.android.features.messages.impl.actionlist package io.element.android.features.messages.impl.actionlist
import io.element.android.features.messages.impl.UserEventPermissions
import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItem
sealed interface ActionListEvents { sealed interface ActionListEvents {
data object Clear : ActionListEvents data object Clear : ActionListEvents
data class ComputeForMessage( data class ComputeForMessage(
val event: TimelineItem.Event, val event: TimelineItem.Event,
val canRedactOwn: Boolean, val userEventPermissions: UserEventPermissions,
val canRedactOther: Boolean,
val canSendMessage: Boolean,
val canSendReaction: Boolean,
) : ActionListEvents ) : ActionListEvents
} }

View file

@ -23,15 +23,17 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import io.element.android.features.messages.impl.UserEventPermissions
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.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
import io.element.android.features.messages.impl.timeline.model.event.canBeCopied import io.element.android.features.messages.impl.timeline.model.event.canBeCopied
import io.element.android.features.messages.impl.timeline.model.event.canBeForwarded
import io.element.android.features.messages.impl.timeline.model.event.canReact import io.element.android.features.messages.impl.timeline.model.event.canReact
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.preferences.api.store.AppPreferencesStore
@ -58,10 +60,7 @@ class ActionListPresenter @Inject constructor(
ActionListEvents.Clear -> target.value = ActionListState.Target.None ActionListEvents.Clear -> target.value = ActionListState.Target.None
is ActionListEvents.ComputeForMessage -> localCoroutineScope.computeForMessage( is ActionListEvents.ComputeForMessage -> localCoroutineScope.computeForMessage(
timelineItem = event.event, timelineItem = event.event,
userCanRedactOwn = event.canRedactOwn, usersEventPermissions = event.userEventPermissions,
userCanRedactOther = event.canRedactOther,
userCanSendMessage = event.canSendMessage,
userCanSendReaction = event.canSendReaction,
isDeveloperModeEnabled = isDeveloperModeEnabled, isDeveloperModeEnabled = isDeveloperModeEnabled,
target = target, target = target,
) )
@ -76,136 +75,18 @@ class ActionListPresenter @Inject constructor(
private fun CoroutineScope.computeForMessage( private fun CoroutineScope.computeForMessage(
timelineItem: TimelineItem.Event, timelineItem: TimelineItem.Event,
userCanRedactOwn: Boolean, usersEventPermissions: UserEventPermissions,
userCanRedactOther: Boolean,
userCanSendMessage: Boolean,
userCanSendReaction: Boolean,
isDeveloperModeEnabled: Boolean, isDeveloperModeEnabled: Boolean,
target: MutableState<ActionListState.Target> target: MutableState<ActionListState.Target>
) = launch { ) = launch {
target.value = ActionListState.Target.Loading(timelineItem) target.value = ActionListState.Target.Loading(timelineItem)
val canRedact = timelineItem.isMine && userCanRedactOwn || !timelineItem.isMine && userCanRedactOther
val actions = val actions = buildActions(
when (timelineItem.content) { timelineItem = timelineItem,
is TimelineItemCallNotifyContent -> { usersEventPermissions = usersEventPermissions,
if (isDeveloperModeEnabled) { isDeveloperModeEnabled = isDeveloperModeEnabled,
listOf(TimelineItemAction.ViewSource) )
} else { val displayEmojiReactions = usersEventPermissions.canSendReaction &&
emptyList()
}
}
is TimelineItemRedactedContent -> {
if (isDeveloperModeEnabled) {
listOf(TimelineItemAction.ViewSource)
} else {
emptyList()
}
}
is TimelineItemStateContent -> {
buildList {
add(TimelineItemAction.Copy)
if (timelineItem.isRemote) {
add(TimelineItemAction.CopyLink)
}
if (isDeveloperModeEnabled) {
add(TimelineItemAction.ViewSource)
}
}
}
is TimelineItemPollContent -> {
val canEndPoll = timelineItem.isRemote &&
!timelineItem.content.isEnded &&
(timelineItem.isMine || canRedact)
buildList {
if (timelineItem.isRemote) {
// Can only reply or forward messages already uploaded to the server
add(TimelineItemAction.Reply)
}
if (timelineItem.isRemote && timelineItem.isEditable) {
add(TimelineItemAction.Edit)
}
if (canEndPoll) {
add(TimelineItemAction.EndPoll)
}
if (timelineItem.content.canBeCopied()) {
add(TimelineItemAction.Copy)
}
if (timelineItem.isRemote) {
add(TimelineItemAction.CopyLink)
}
if (isDeveloperModeEnabled) {
add(TimelineItemAction.ViewSource)
}
if (!timelineItem.isMine) {
add(TimelineItemAction.ReportContent)
}
if (canRedact) {
add(TimelineItemAction.Redact)
}
}
}
is TimelineItemVoiceContent -> {
buildList {
if (timelineItem.isRemote) {
add(TimelineItemAction.Reply)
add(TimelineItemAction.Forward)
add(TimelineItemAction.CopyLink)
}
if (isDeveloperModeEnabled) {
add(TimelineItemAction.ViewSource)
}
if (!timelineItem.isMine) {
add(TimelineItemAction.ReportContent)
}
if (canRedact) {
add(TimelineItemAction.Redact)
}
}
}
is TimelineItemLegacyCallInviteContent -> {
buildList {
if (isDeveloperModeEnabled) {
add(TimelineItemAction.ViewSource)
}
}
}
else -> buildList<TimelineItemAction> {
if (timelineItem.isRemote) {
// Can only reply or forward messages already uploaded to the server
if (userCanSendMessage) {
if (timelineItem.isThreaded) {
add(TimelineItemAction.ReplyInThread)
} else {
add(TimelineItemAction.Reply)
}
}
// Stickers can't be forwarded (yet) so we don't show the option
// See https://github.com/element-hq/element-x-android/issues/2161
if (!timelineItem.isSticker) {
add(TimelineItemAction.Forward)
}
}
if (timelineItem.isEditable) {
add(TimelineItemAction.Edit)
}
if (timelineItem.content.canBeCopied()) {
add(TimelineItemAction.Copy)
}
if (timelineItem.isRemote) {
add(TimelineItemAction.CopyLink)
}
if (isDeveloperModeEnabled) {
add(TimelineItemAction.ViewSource)
}
if (!timelineItem.isMine) {
add(TimelineItemAction.ReportContent)
}
if (canRedact) {
add(TimelineItemAction.Redact)
}
}
}
val displayEmojiReactions = userCanSendReaction &&
timelineItem.isRemote && timelineItem.isRemote &&
timelineItem.content.canReact() timelineItem.content.canReact()
if (actions.isNotEmpty() || displayEmojiReactions) { if (actions.isNotEmpty() || displayEmojiReactions) {
@ -219,3 +100,61 @@ class ActionListPresenter @Inject constructor(
} }
} }
} }
private fun buildActions(
timelineItem: TimelineItem.Event,
usersEventPermissions: UserEventPermissions,
isDeveloperModeEnabled: Boolean,
): List<TimelineItemAction> {
val canRedact = timelineItem.isMine && usersEventPermissions.canRedactOwn || !timelineItem.isMine && usersEventPermissions.canRedactOther
return buildList {
if (timelineItem.canBeRepliedTo && usersEventPermissions.canSendMessage) {
if (timelineItem.isThreaded) {
add(TimelineItemAction.ReplyInThread)
} else {
add(TimelineItemAction.Reply)
}
}
if (timelineItem.isRemote && timelineItem.content.canBeForwarded()) {
add(TimelineItemAction.Forward)
}
if (timelineItem.isEditable) {
add(TimelineItemAction.Edit)
}
if (canRedact && timelineItem.content is TimelineItemPollContent && !timelineItem.content.isEnded) {
add(TimelineItemAction.EndPoll)
}
if (timelineItem.content.canBeCopied()) {
add(TimelineItemAction.Copy)
}
if (timelineItem.isRemote) {
add(TimelineItemAction.CopyLink)
}
if (isDeveloperModeEnabled) {
add(TimelineItemAction.ViewSource)
}
if (!timelineItem.isMine) {
add(TimelineItemAction.ReportContent)
}
if (canRedact) {
add(TimelineItemAction.Redact)
}
}.postFilter(timelineItem.content)
}
/**
* Post filter the actions based on the content of the event.
*/
private fun List<TimelineItemAction>.postFilter(content: TimelineItemEventContent): List<TimelineItemAction> {
return filter { action ->
when (content) {
is TimelineItemCallNotifyContent,
is TimelineItemLegacyCallInviteContent,
is TimelineItemStateContent,
is TimelineItemRedactedContent -> {
action == TimelineItemAction.ViewSource
}
else -> true
}
}
}

View file

@ -127,6 +127,7 @@ internal fun aTimelineItemEvent(
transactionId: TransactionId? = null, transactionId: TransactionId? = null,
isMine: Boolean = false, isMine: Boolean = false,
isEditable: Boolean = false, isEditable: Boolean = false,
canBeRepliedTo: Boolean = false,
senderDisplayName: String = "Sender", senderDisplayName: String = "Sender",
displayNameAmbiguous: Boolean = false, displayNameAmbiguous: Boolean = false,
content: TimelineItemEventContent = aTimelineItemTextContent(), content: TimelineItemEventContent = aTimelineItemTextContent(),
@ -150,6 +151,7 @@ internal fun aTimelineItemEvent(
sentTime = "12:34", sentTime = "12:34",
isMine = isMine, isMine = isMine,
isEditable = isEditable, isEditable = isEditable,
canBeRepliedTo = canBeRepliedTo,
senderProfile = aProfileTimelineDetailsReady( senderProfile = aProfileTimelineDetailsReady(
displayName = senderDisplayName, displayName = senderDisplayName,
displayNameAmbiguous = displayNameAmbiguous, displayNameAmbiguous = displayNameAmbiguous,

View file

@ -77,7 +77,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
import io.element.android.features.messages.impl.timeline.model.event.canBeRepliedTo
import io.element.android.libraries.designsystem.colors.AvatarColorsProvider import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
import io.element.android.libraries.designsystem.components.EqualWidthColumn import io.element.android.libraries.designsystem.components.EqualWidthColumn
import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.Avatar
@ -148,7 +147,7 @@ fun TimelineItemEventRow(
} else { } else {
Spacer(modifier = Modifier.height(2.dp)) Spacer(modifier = Modifier.height(2.dp))
} }
val canReply = timelineRoomInfo.userHasPermissionToSendMessage && event.content.canBeRepliedTo() val canReply = timelineRoomInfo.userHasPermissionToSendMessage && event.canBeRepliedTo
if (canReply) { if (canReply) {
val state: SwipeableActionsState = rememberSwipeableActionsState() val state: SwipeableActionsState = rememberSwipeableActionsState()
val offset = state.offset.floatValue val offset = state.offset.floatValue

View file

@ -76,6 +76,7 @@ class TimelineItemEventFactory @Inject constructor(
content = contentFactory.create(currentTimelineItem.event), content = contentFactory.create(currentTimelineItem.event),
isMine = currentTimelineItem.event.isOwn, isMine = currentTimelineItem.event.isOwn,
isEditable = currentTimelineItem.event.isEditable, isEditable = currentTimelineItem.event.isEditable,
canBeRepliedTo = currentTimelineItem.event.canBeRepliedTo,
sentTime = sentTime, sentTime = sentTime,
groupPosition = groupPosition, groupPosition = groupPosition,
reactionsState = currentTimelineItem.computeReactionsState(), reactionsState = currentTimelineItem.computeReactionsState(),

View file

@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.timeline.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel
@ -74,6 +75,7 @@ sealed interface TimelineItem {
val sentTime: String = "", val sentTime: String = "",
val isMine: Boolean = false, val isMine: Boolean = false,
val isEditable: Boolean, val isEditable: Boolean,
val canBeRepliedTo: Boolean,
val groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None, val groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None,
val reactionsState: TimelineItemReactions, val reactionsState: TimelineItemReactions,
val readReceiptState: TimelineItemReadReceipts, val readReceiptState: TimelineItemReadReceipts,
@ -94,6 +96,7 @@ sealed interface TimelineItem {
val isSticker: Boolean = content is TimelineItemStickerContent val isSticker: Boolean = content is TimelineItemStickerContent
val isRemote = eventId != null val isRemote = eventId != null
} }
@Immutable @Immutable

View file

@ -24,27 +24,27 @@ sealed interface TimelineItemEventContent {
} }
/** /**
* Only text based content and states can be copied. * Only text based content can be copied.
*/ */
fun TimelineItemEventContent.canBeCopied(): Boolean = fun TimelineItemEventContent.canBeCopied(): Boolean =
when (this) { this is TimelineItemTextBasedContent
is TimelineItemTextBasedContent,
is TimelineItemStateContent,
is TimelineItemRedactedContent -> true
else -> false
}
/** /**
* Determine if the event content can be replied to. * Returns true if the event content can be forwarded.
* Note: it should match the logic in [io.element.android.features.messages.impl.actionlist.ActionListPresenter].
*/ */
fun TimelineItemEventContent.canBeRepliedTo(): Boolean = fun TimelineItemEventContent.canBeForwarded(): Boolean =
when (this) { when (this) {
is TimelineItemRedactedContent, is TimelineItemTextBasedContent,
is TimelineItemLegacyCallInviteContent, is TimelineItemImageContent,
is TimelineItemCallNotifyContent, is TimelineItemFileContent,
is TimelineItemStateContent -> false is TimelineItemAudioContent,
else -> true is TimelineItemVideoContent,
is TimelineItemLocationContent,
is TimelineItemVoiceContent -> true
// Stickers can't be forwarded (yet) so we don't show the option
// See https://github.com/element-hq/element-x-android/issues/2161
is TimelineItemStickerContent -> false
else -> false
} }
/** /**

View file

@ -137,8 +137,8 @@ class MessagesPresenterTest {
assertThat(initialState.roomName).isEqualTo(AsyncData.Success("")) assertThat(initialState.roomName).isEqualTo(AsyncData.Success(""))
assertThat(initialState.roomAvatar) assertThat(initialState.roomAvatar)
.isEqualTo(AsyncData.Success(AvatarData(id = A_ROOM_ID.value, name = "", url = AN_AVATAR_URL, size = AvatarSize.TimelineRoom))) .isEqualTo(AsyncData.Success(AvatarData(id = A_ROOM_ID.value, name = "", url = AN_AVATAR_URL, size = AvatarSize.TimelineRoom)))
assertThat(initialState.userHasPermissionToSendMessage).isTrue() assertThat(initialState.userEventPermissions.canSendMessage).isTrue()
assertThat(initialState.userHasPermissionToRedactOwn).isTrue() assertThat(initialState.userEventPermissions.canRedactOwn).isTrue()
assertThat(initialState.hasNetworkConnection).isTrue() assertThat(initialState.hasNetworkConnection).isTrue()
assertThat(initialState.snackbarMessage).isNull() assertThat(initialState.snackbarMessage).isNull()
assertThat(initialState.inviteProgress).isEqualTo(AsyncData.Uninitialized) assertThat(initialState.inviteProgress).isEqualTo(AsyncData.Uninitialized)
@ -787,7 +787,7 @@ class MessagesPresenterTest {
presenter.present() presenter.present()
}.test { }.test {
val state = awaitFirstItem() val state = awaitFirstItem()
assertThat(state.userHasPermissionToSendMessage).isTrue() assertThat(state.userEventPermissions.canSendMessage).isTrue()
} }
} }
@ -811,9 +811,9 @@ class MessagesPresenterTest {
presenter.present() presenter.present()
}.test { }.test {
// Default value // Default value
assertThat(awaitItem().userHasPermissionToSendMessage).isTrue() assertThat(awaitItem().userEventPermissions.canSendMessage).isTrue()
skipItems(1) skipItems(1)
assertThat(awaitItem().userHasPermissionToSendMessage).isFalse() assertThat(awaitItem().userEventPermissions.canSendMessage).isFalse()
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
} }
} }
@ -831,9 +831,9 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = consumeItemsUntilPredicate { it.userHasPermissionToRedactOwn }.last() val initialState = consumeItemsUntilPredicate { it.userEventPermissions.canRedactOwn }.last()
assertThat(initialState.userHasPermissionToRedactOwn).isTrue() assertThat(initialState.userEventPermissions.canRedactOwn).isTrue()
assertThat(initialState.userHasPermissionToRedactOther).isFalse() assertThat(initialState.userEventPermissions.canRedactOther).isFalse()
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
} }
} }
@ -851,9 +851,9 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = consumeItemsUntilPredicate { it.userHasPermissionToRedactOther }.last() val initialState = consumeItemsUntilPredicate { it.userEventPermissions.canRedactOther }.last()
assertThat(initialState.userHasPermissionToRedactOwn).isFalse() assertThat(initialState.userEventPermissions.canRedactOwn).isFalse()
assertThat(initialState.userHasPermissionToRedactOther).isTrue() assertThat(initialState.userEventPermissions.canRedactOther).isTrue()
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
} }
} }
@ -963,6 +963,7 @@ class MessagesPresenterTest {
room = matrixRoom, room = matrixRoom,
sessionPreferencesStore = sessionPreferencesStore, sessionPreferencesStore = sessionPreferencesStore,
) )
val readReceiptBottomSheetPresenter = ReadReceiptBottomSheetPresenter() val readReceiptBottomSheetPresenter = ReadReceiptBottomSheetPresenter()
val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider()) val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider())
val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom) val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom)

View file

@ -26,6 +26,7 @@ import androidx.compose.ui.test.onAllNodesWithContentDescription
import androidx.compose.ui.test.onAllNodesWithTag import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onFirst import androidx.compose.ui.test.onFirst
import androidx.compose.ui.test.onLast
import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
@ -43,7 +44,6 @@ import io.element.android.features.messages.impl.actionlist.model.TimelineItemAc
import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
import io.element.android.features.messages.impl.timeline.aTimelineItemList
import io.element.android.features.messages.impl.timeline.aTimelineItemReadReceipts import io.element.android.features.messages.impl.timeline.aTimelineItemReadReceipts
import io.element.android.features.messages.impl.timeline.aTimelineRoomInfo import io.element.android.features.messages.impl.timeline.aTimelineRoomInfo
import io.element.android.features.messages.impl.timeline.aTimelineState import io.element.android.features.messages.impl.timeline.aTimelineState
@ -175,10 +175,12 @@ class MessagesViewTest {
actionListState = anActionListState( actionListState = anActionListState(
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
userHasPermissionToSendMessage = userHasPermissionToSendMessage, userEventPermissions = UserEventPermissions(
userHasPermissionToRedactOwn = userHasPermissionToRedactOwn, canSendMessage = userHasPermissionToSendMessage,
userHasPermissionToRedactOther = userHasPermissionToRedactOther, canRedactOwn = userHasPermissionToRedactOwn,
userHasPermissionToSendReaction = userHasPermissionToSendReaction, canRedactOther = userHasPermissionToRedactOther,
canSendReaction = userHasPermissionToSendReaction,
),
) )
val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event
rule.setMessagesView( rule.setMessagesView(
@ -189,10 +191,7 @@ class MessagesViewTest {
eventsRecorder.assertSingle( eventsRecorder.assertSingle(
ActionListEvents.ComputeForMessage( ActionListEvents.ComputeForMessage(
event = timelineItem, event = timelineItem,
canRedactOwn = state.userHasPermissionToRedactOwn, userEventPermissions = state.userEventPermissions,
canRedactOther = state.userHasPermissionToRedactOther,
canSendMessage = state.userHasPermissionToSendMessage,
canSendReaction = state.userHasPermissionToSendReaction,
) )
) )
} }
@ -237,9 +236,11 @@ class MessagesViewTest {
private fun swipeTest(userHasPermissionToSendMessage: Boolean) { private fun swipeTest(userHasPermissionToSendMessage: Boolean) {
val eventsRecorder = EventsRecorder<MessagesEvents>() val eventsRecorder = EventsRecorder<MessagesEvents>()
val canBeRepliedEvent = aTimelineItemEvent(canBeRepliedTo = true)
val cannotBeRepliedEvent = aTimelineItemEvent(canBeRepliedTo = false)
val state = aMessagesState( val state = aMessagesState(
timelineState = aTimelineState( timelineState = aTimelineState(
timelineItems = aTimelineItemList(aTimelineItemTextContent()), timelineItems = persistentListOf(canBeRepliedEvent, cannotBeRepliedEvent),
timelineRoomInfo = aTimelineRoomInfo( timelineRoomInfo = aTimelineRoomInfo(
userHasPermissionToSendMessage = userHasPermissionToSendMessage userHasPermissionToSendMessage = userHasPermissionToSendMessage
), ),
@ -249,10 +250,12 @@ class MessagesViewTest {
rule.setMessagesView( rule.setMessagesView(
state = state, state = state,
) )
rule.onAllNodesWithTag(TestTags.messageBubble.value).onFirst().performTouchInput { swipeRight(endX = 200f) } rule.onAllNodesWithTag(TestTags.messageBubble.value).apply {
onFirst().performTouchInput { swipeRight(endX = 200f) }
onLast().performTouchInput { swipeRight(endX = 200f) }
}
if (userHasPermissionToSendMessage) { if (userHasPermissionToSendMessage) {
val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event eventsRecorder.assertSingle(MessagesEvents.HandleAction(TimelineItemAction.Reply, canBeRepliedEvent))
eventsRecorder.assertSingle(MessagesEvents.HandleAction(TimelineItemAction.Reply, timelineItem))
} else { } else {
eventsRecorder.assertEmpty() eventsRecorder.assertEmpty()
} }

View file

@ -20,6 +20,7 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.messages.impl.aUserEventPermissions
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.fixtures.aMessageEvent import io.element.android.features.messages.impl.fixtures.aMessageEvent
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
@ -31,6 +32,7 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent
import io.element.android.features.poll.api.pollcontent.aPollAnswerItemList import io.element.android.features.poll.api.pollcontent.aPollAnswerItemList
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.test.A_MESSAGE import io.element.android.libraries.matrix.test.A_MESSAGE
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
@ -66,10 +68,12 @@ class ActionListPresenterTest {
initialState.eventSink.invoke( initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage( ActionListEvents.ComputeForMessage(
event = messageEvent, event = messageEvent,
canRedactOwn = false, userEventPermissions = aUserEventPermissions(
canRedactOther = false, canRedactOwn = false,
canSendMessage = true, canRedactOther = false,
canSendReaction = true, canSendMessage = true,
canSendReaction = true,
)
) )
) )
// val loadingState = awaitItem() // val loadingState = awaitItem()
@ -104,10 +108,12 @@ class ActionListPresenterTest {
initialState.eventSink.invoke( initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage( ActionListEvents.ComputeForMessage(
event = messageEvent, event = messageEvent,
canRedactOwn = false, userEventPermissions = aUserEventPermissions(
canRedactOther = false, canRedactOwn = false,
canSendMessage = true, canRedactOther = false,
canSendReaction = true, canSendMessage = true,
canSendReaction = true,
)
) )
) )
// val loadingState = awaitItem() // val loadingState = awaitItem()
@ -142,10 +148,12 @@ class ActionListPresenterTest {
initialState.eventSink.invoke( initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage( ActionListEvents.ComputeForMessage(
event = messageEvent, event = messageEvent,
canRedactOwn = false, userEventPermissions = aUserEventPermissions(
canRedactOther = false, canRedactOwn = false,
canSendMessage = true, canRedactOther = false,
canSendReaction = true, canSendMessage = true,
canSendReaction = true,
)
) )
) )
// val loadingState = awaitItem() // val loadingState = awaitItem()
@ -185,10 +193,12 @@ class ActionListPresenterTest {
initialState.eventSink.invoke( initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage( ActionListEvents.ComputeForMessage(
event = messageEvent, event = messageEvent,
canRedactOwn = true, userEventPermissions = aUserEventPermissions(
canRedactOther = false, canRedactOwn = true,
canSendMessage = false, canRedactOther = false,
canSendReaction = true canSendMessage = false,
canSendReaction = true
)
) )
) )
// val loadingState = awaitItem() // val loadingState = awaitItem()
@ -227,10 +237,12 @@ class ActionListPresenterTest {
initialState.eventSink.invoke( initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage( ActionListEvents.ComputeForMessage(
event = messageEvent, event = messageEvent,
canRedactOwn = false, userEventPermissions = aUserEventPermissions(
canRedactOther = true, canRedactOwn = false,
canSendMessage = true, canRedactOther = true,
canSendReaction = true, canSendMessage = true,
canSendReaction = true,
)
) )
) )
val successState = awaitItem() val successState = awaitItem()
@ -269,10 +281,12 @@ class ActionListPresenterTest {
initialState.eventSink.invoke( initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage( ActionListEvents.ComputeForMessage(
event = messageEvent, event = messageEvent,
canRedactOwn = false, userEventPermissions = aUserEventPermissions(
canRedactOther = true, canRedactOwn = false,
canSendMessage = true, canRedactOther = true,
canSendReaction = false canSendMessage = true,
canSendReaction = false
)
) )
) )
val successState = awaitItem() val successState = awaitItem()
@ -310,10 +324,12 @@ class ActionListPresenterTest {
initialState.eventSink.invoke( initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage( ActionListEvents.ComputeForMessage(
event = messageEvent, event = messageEvent,
canRedactOwn = true, userEventPermissions = aUserEventPermissions(
canRedactOther = false, canRedactOwn = true,
canSendMessage = true, canRedactOther = false,
canSendReaction = true, canSendMessage = true,
canSendReaction = true,
)
) )
) )
// val loadingState = awaitItem() // val loadingState = awaitItem()
@ -353,10 +369,12 @@ class ActionListPresenterTest {
initialState.eventSink.invoke( initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage( ActionListEvents.ComputeForMessage(
event = messageEvent, event = messageEvent,
canRedactOwn = false, userEventPermissions = aUserEventPermissions(
canRedactOther = false, canRedactOwn = false,
canSendMessage = true, canRedactOther = false,
canSendReaction = true, canSendMessage = true,
canSendReaction = true,
)
) )
) )
// val loadingState = awaitItem() // val loadingState = awaitItem()
@ -396,10 +414,12 @@ class ActionListPresenterTest {
initialState.eventSink.invoke( initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage( ActionListEvents.ComputeForMessage(
event = messageEvent, event = messageEvent,
canRedactOwn = true, userEventPermissions = aUserEventPermissions(
canRedactOther = false, canRedactOwn = true,
canSendMessage = true, canRedactOther = false,
canSendReaction = true, canSendMessage = true,
canSendReaction = true,
),
) )
) )
// val loadingState = awaitItem() // val loadingState = awaitItem()
@ -437,10 +457,12 @@ class ActionListPresenterTest {
initialState.eventSink.invoke( initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage( ActionListEvents.ComputeForMessage(
event = stateEvent, event = stateEvent,
canRedactOwn = false, userEventPermissions = aUserEventPermissions(
canRedactOther = false, canRedactOwn = false,
canSendMessage = true, canRedactOther = false,
canSendReaction = true, canSendMessage = true,
canSendReaction = true,
)
) )
) )
// val loadingState = awaitItem() // val loadingState = awaitItem()
@ -451,8 +473,6 @@ class ActionListPresenterTest {
event = stateEvent, event = stateEvent,
displayEmojiReactions = false, displayEmojiReactions = false,
actions = persistentListOf( actions = persistentListOf(
TimelineItemAction.Copy,
TimelineItemAction.CopyLink,
TimelineItemAction.ViewSource, TimelineItemAction.ViewSource,
) )
) )
@ -476,26 +496,16 @@ class ActionListPresenterTest {
initialState.eventSink.invoke( initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage( ActionListEvents.ComputeForMessage(
event = stateEvent, event = stateEvent,
canRedactOwn = false, userEventPermissions = aUserEventPermissions(
canRedactOther = false, canRedactOwn = false,
canSendMessage = true, canRedactOther = false,
canSendReaction = true, canSendMessage = true,
canSendReaction = true,
)
) )
) )
// val loadingState = awaitItem() // val loadingState = awaitItem()
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
val successState = awaitItem()
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
event = stateEvent,
displayEmojiReactions = false,
actions = persistentListOf(
TimelineItemAction.Copy,
TimelineItemAction.CopyLink,
)
)
)
initialState.eventSink.invoke(ActionListEvents.Clear)
assertThat(awaitItem().target).isEqualTo(ActionListState.Target.None) assertThat(awaitItem().target).isEqualTo(ActionListState.Target.None)
} }
} }
@ -514,10 +524,12 @@ class ActionListPresenterTest {
initialState.eventSink.invoke( initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage( ActionListEvents.ComputeForMessage(
event = messageEvent, event = messageEvent,
canRedactOwn = true, userEventPermissions = aUserEventPermissions(
canRedactOther = false, canRedactOwn = true,
canSendMessage = true, canRedactOther = false,
canSendReaction = true, canSendMessage = true,
canSendReaction = true,
)
) )
) )
// val loadingState = awaitItem() // val loadingState = awaitItem()
@ -561,10 +573,12 @@ class ActionListPresenterTest {
initialState.eventSink.invoke( initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage( ActionListEvents.ComputeForMessage(
event = messageEvent, event = messageEvent,
canRedactOwn = false, userEventPermissions = aUserEventPermissions(
canRedactOther = false, canRedactOwn = false,
canSendMessage = true, canRedactOther = false,
canSendReaction = true, canSendMessage = true,
canSendReaction = true,
)
) )
) )
assertThat(awaitItem().target).isInstanceOf(ActionListState.Target.Success::class.java) assertThat(awaitItem().target).isInstanceOf(ActionListState.Target.Success::class.java)
@ -572,10 +586,12 @@ class ActionListPresenterTest {
initialState.eventSink.invoke( initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage( ActionListEvents.ComputeForMessage(
event = redactedEvent, event = redactedEvent,
canRedactOwn = false, userEventPermissions = aUserEventPermissions(
canRedactOther = false, canRedactOwn = false,
canSendMessage = true, canRedactOther = false,
canSendReaction = true, canSendMessage = true,
canSendReaction = true,
)
) )
) )
awaitItem().run { awaitItem().run {
@ -595,16 +611,19 @@ class ActionListPresenterTest {
// No event id, so it's not sent yet // No event id, so it's not sent yet
eventId = null, eventId = null,
isMine = true, isMine = true,
canBeRepliedTo = false,
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null), content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null),
) )
initialState.eventSink.invoke( initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage( ActionListEvents.ComputeForMessage(
event = messageEvent, event = messageEvent,
canRedactOwn = true, userEventPermissions = aUserEventPermissions(
canRedactOther = false, canRedactOwn = true,
canSendMessage = true, canRedactOther = false,
canSendReaction = true, canSendMessage = true,
canSendReaction = true,
)
) )
) )
val successState = awaitItem() val successState = awaitItem()
@ -637,10 +656,12 @@ class ActionListPresenterTest {
initialState.eventSink.invoke( initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage( ActionListEvents.ComputeForMessage(
event = messageEvent, event = messageEvent,
canRedactOwn = true, userEventPermissions = aUserEventPermissions(
canRedactOther = false, canRedactOwn = true,
canSendMessage = true, canRedactOther = false,
canSendReaction = true, canSendMessage = true,
canSendReaction = true,
)
) )
) )
val successState = awaitItem() val successState = awaitItem()
@ -675,10 +696,12 @@ class ActionListPresenterTest {
initialState.eventSink.invoke( initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage( ActionListEvents.ComputeForMessage(
event = messageEvent, event = messageEvent,
canRedactOwn = true, userEventPermissions = aUserEventPermissions(
canRedactOther = false, canRedactOwn = true,
canSendMessage = true, canRedactOther = false,
canSendReaction = true, canSendMessage = true,
canSendReaction = true,
)
) )
) )
val successState = awaitItem() val successState = awaitItem()
@ -712,10 +735,12 @@ class ActionListPresenterTest {
initialState.eventSink.invoke( initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage( ActionListEvents.ComputeForMessage(
event = messageEvent, event = messageEvent,
canRedactOwn = true, userEventPermissions = aUserEventPermissions(
canRedactOther = false, canRedactOwn = true,
canSendMessage = true, canRedactOther = false,
canSendReaction = true, canSendMessage = true,
canSendReaction = true,
)
) )
) )
val successState = awaitItem() val successState = awaitItem()
@ -742,15 +767,18 @@ class ActionListPresenterTest {
val initialState = awaitItem() val initialState = awaitItem()
val messageEvent = aMessageEvent( val messageEvent = aMessageEvent(
isMine = true, isMine = true,
isEditable = false,
content = aTimelineItemVoiceContent(), content = aTimelineItemVoiceContent(),
) )
initialState.eventSink.invoke( initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage( ActionListEvents.ComputeForMessage(
event = messageEvent, event = messageEvent,
canRedactOwn = true, userEventPermissions = aUserEventPermissions(
canRedactOther = false, canRedactOwn = true,
canSendMessage = true, canRedactOther = false,
canSendReaction = true, canSendMessage = true,
canSendReaction = true,
)
) )
) )
val successState = awaitItem() val successState = awaitItem()
@ -783,10 +811,12 @@ class ActionListPresenterTest {
initialState.eventSink.invoke( initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage( ActionListEvents.ComputeForMessage(
event = messageEvent, event = messageEvent,
canRedactOwn = true, userEventPermissions = aUserEventPermissions(
canRedactOther = false, canRedactOwn = true,
canSendMessage = true, canRedactOther = false,
canSendReaction = true, canSendMessage = true,
canSendReaction = true,
)
) )
) )
val successState = awaitItem() val successState = awaitItem()

View file

@ -42,6 +42,7 @@ internal fun aMessageEvent(
transactionId: TransactionId? = null, transactionId: TransactionId? = null,
isMine: Boolean = true, isMine: Boolean = true,
isEditable: Boolean = true, isEditable: Boolean = true,
canBeRepliedTo: Boolean = true,
content: TimelineItemEventContent = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, formattedBody = null, isEdited = false), content: TimelineItemEventContent = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, formattedBody = null, isEdited = false),
inReplyTo: InReplyToDetails? = null, inReplyTo: InReplyToDetails? = null,
isThreaded: Boolean = false, isThreaded: Boolean = false,
@ -58,6 +59,7 @@ internal fun aMessageEvent(
sentTime = "", sentTime = "",
isMine = isMine, isMine = isMine,
isEditable = isEditable, isEditable = isEditable,
canBeRepliedTo = canBeRepliedTo,
reactionsState = aTimelineItemReactions(count = 0), reactionsState = aTimelineItemReactions(count = 0),
readReceiptState = TimelineItemReadReceipts(emptyList<ReadReceiptData>().toImmutableList()), readReceiptState = TimelineItemReadReceipts(emptyList<ReadReceiptData>().toImmutableList()),
localSendState = sendState, localSendState = sendState,

View file

@ -46,6 +46,7 @@ class TimelineItemGrouperTest {
readReceiptState = TimelineItemReadReceipts(emptyList<ReadReceiptData>().toImmutableList()), readReceiptState = TimelineItemReadReceipts(emptyList<ReadReceiptData>().toImmutableList()),
localSendState = LocalEventSendState.Sent(AN_EVENT_ID), localSendState = LocalEventSendState.Sent(AN_EVENT_ID),
isEditable = false, isEditable = false,
canBeRepliedTo = false,
inReplyTo = null, inReplyTo = null,
isThreaded = false, isThreaded = false,
debugInfo = aTimelineItemDebugInfo(), debugInfo = aTimelineItemDebugInfo(),

View file

@ -85,6 +85,7 @@ fun aRedactedMatrixTimeline(eventId: EventId) = listOf<MatrixTimelineItem>(
eventId = eventId, eventId = eventId,
transactionId = null, transactionId = null,
isEditable = false, isEditable = false,
canBeRepliedTo = false,
isLocal = false, isLocal = false,
isOwn = false, isOwn = false,
isRemote = false, isRemote = false,

View file

@ -26,6 +26,7 @@ data class EventTimelineItem(
val eventId: EventId?, val eventId: EventId?,
val transactionId: TransactionId?, val transactionId: TransactionId?,
val isEditable: Boolean, val isEditable: Boolean,
val canBeRepliedTo: Boolean,
val isLocal: Boolean, val isLocal: Boolean,
val isOwn: Boolean, val isOwn: Boolean,
val isRemote: Boolean, val isRemote: Boolean,

View file

@ -44,6 +44,7 @@ class EventTimelineItemMapper(private val contentMapper: TimelineEventContentMap
eventId = it.eventId()?.let(::EventId), eventId = it.eventId()?.let(::EventId),
transactionId = it.transactionId()?.let(::TransactionId), transactionId = it.transactionId()?.let(::TransactionId),
isEditable = it.isEditable(), isEditable = it.isEditable(),
canBeRepliedTo = it.canBeRepliedTo(),
isLocal = it.isLocal(), isLocal = it.isLocal(),
isOwn = it.isOwn(), isOwn = it.isOwn(),
isRemote = it.isRemote(), isRemote = it.isRemote(),

View file

@ -46,6 +46,7 @@ fun anEventTimelineItem(
eventId: EventId = AN_EVENT_ID, eventId: EventId = AN_EVENT_ID,
transactionId: TransactionId? = null, transactionId: TransactionId? = null,
isEditable: Boolean = false, isEditable: Boolean = false,
canBeRepliedTo: Boolean = false,
isLocal: Boolean = false, isLocal: Boolean = false,
isOwn: Boolean = false, isOwn: Boolean = false,
isRemote: Boolean = false, isRemote: Boolean = false,
@ -61,6 +62,7 @@ fun anEventTimelineItem(
eventId = eventId, eventId = eventId,
transactionId = transactionId, transactionId = transactionId,
isEditable = isEditable, isEditable = isEditable,
canBeRepliedTo = canBeRepliedTo,
isLocal = isLocal, isLocal = isLocal,
isOwn = isOwn, isOwn = isOwn,
isRemote = isRemote, isRemote = isRemote,