From 0149007dd407f88f03572bd4d64830139ee4966b Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 26 Jul 2024 18:20:03 +0200 Subject: [PATCH] Timeline Action : refactor how it's computed and align with iOS --- .../messages/impl/MessagesPresenter.kt | 31 ++- .../features/messages/impl/MessagesState.kt | 5 +- .../messages/impl/MessagesStateProvider.kt | 24 +- .../features/messages/impl/MessagesView.kt | 7 +- .../messages/impl/UserEventPermissions.kt | 28 +++ .../impl/actionlist/ActionListEvents.kt | 6 +- .../impl/actionlist/ActionListPresenter.kt | 201 ++++++---------- .../impl/timeline/TimelineStateProvider.kt | 2 + .../components/TimelineItemEventRow.kt | 3 +- .../event/TimelineItemEventFactory.kt | 1 + .../impl/timeline/model/TimelineItem.kt | 3 + .../model/event/TimelineItemEventContent.kt | 30 +-- .../messages/impl/MessagesPresenterTest.kt | 23 +- .../messages/impl/MessagesViewTest.kt | 29 +-- .../actionlist/ActionListPresenterTest.kt | 218 ++++++++++-------- .../impl/fixtures/MessageEventFixtures.kt | 2 + .../groups/TimelineItemGrouperTest.kt | 1 + .../RedactedVoiceMessageManagerTest.kt | 1 + .../timeline/item/event/EventTimelineItem.kt | 1 + .../item/event/EventTimelineItemMapper.kt | 1 + .../matrix/test/timeline/TimelineFixture.kt | 2 + 21 files changed, 323 insertions(+), 296 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/UserEventPermissions.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index b13a95d4f2..2a1ea9dd51 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -20,6 +20,7 @@ import android.os.Build import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -131,10 +132,9 @@ class MessagesPresenter @AssistedInject constructor( val readReceiptBottomSheetState = readReceiptBottomSheetPresenter.present() 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 userHasPermissionToRedactOther by room.canRedactOtherAsState(updateKey = syncUpdateFlow.value) - val userHasPermissionToSendReaction by room.canSendMessageAsState(type = MessageEventType.REACTION, updateKey = syncUpdateFlow.value) + + val userEventPermissions by userEventPermissions(syncUpdateFlow.value) + val roomName: AsyncData by remember { derivedStateOf { roomInfo?.name?.let { AsyncData.Success(it) } ?: AsyncData.Uninitialized } } @@ -211,11 +211,8 @@ class MessagesPresenter @AssistedInject constructor( roomName = roomName, roomAvatar = roomAvatar, heroes = heroes, - userHasPermissionToSendMessage = userHasPermissionToSendMessage, - userHasPermissionToRedactOwn = userHasPermissionToRedactOwn, - userHasPermissionToRedactOther = userHasPermissionToRedactOther, - userHasPermissionToSendReaction = userHasPermissionToSendReaction, composerState = composerState, + userEventPermissions = userEventPermissions, voiceMessageComposerState = voiceMessageComposerState, timelineState = timelineState, typingNotificationState = typingNotificationState, @@ -235,6 +232,24 @@ class MessagesPresenter @AssistedInject constructor( ) } + @Composable + private fun userEventPermissions(updateKey: Long): State { + 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 { return AvatarData( id = id.value, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt index e8657d70bd..99e5b50de8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt @@ -37,10 +37,7 @@ data class MessagesState( val roomName: AsyncData, val roomAvatar: AsyncData, val heroes: ImmutableList, - val userHasPermissionToSendMessage: Boolean, - val userHasPermissionToRedactOwn: Boolean, - val userHasPermissionToRedactOther: Boolean, - val userHasPermissionToSendReaction: Boolean, + val userEventPermissions: UserEventPermissions, val composerState: MessageComposerState, val voiceMessageComposerState: VoiceMessageComposerState, val timelineState: TimelineState, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index 1396d3e17c..082edb1ca3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -53,7 +53,7 @@ open class MessagesStateProvider : PreviewParameterProvider { aMessagesState(), aMessagesState(hasNetworkConnection = false), aMessagesState(composerState = aMessageComposerState(showAttachmentSourcePicker = true)), - aMessagesState(userHasPermissionToSendMessage = false), + aMessagesState(userEventPermissions = aUserEventPermissions(canSendMessage = false)), aMessagesState(showReinvitePrompt = true), aMessagesState( roomName = AsyncData.Uninitialized, @@ -93,10 +93,7 @@ open class MessagesStateProvider : PreviewParameterProvider { fun aMessagesState( roomName: AsyncData = AsyncData.Success("Room name"), roomAvatar: AsyncData = AsyncData.Success(AvatarData("!id:domain", "Room name", size = AvatarSize.TimelineRoom)), - userHasPermissionToSendMessage: Boolean = true, - userHasPermissionToRedactOwn: Boolean = false, - userHasPermissionToRedactOther: Boolean = false, - userHasPermissionToSendReaction: Boolean = true, + userEventPermissions: UserEventPermissions = aUserEventPermissions(), composerState: MessageComposerState = aMessageComposerState( textEditorState = TextEditorState.Rich(aRichTextEditorState(initialText = "Hello", initialFocus = true)), isFullScreen = false, @@ -122,10 +119,7 @@ fun aMessagesState( roomName = roomName, roomAvatar = roomAvatar, heroes = persistentListOf(), - userHasPermissionToSendMessage = userHasPermissionToSendMessage, - userHasPermissionToRedactOwn = userHasPermissionToRedactOwn, - userHasPermissionToRedactOther = userHasPermissionToRedactOther, - userHasPermissionToSendReaction = userHasPermissionToSendReaction, + userEventPermissions = userEventPermissions, composerState = composerState, voiceMessageComposerState = voiceMessageComposerState, typingNotificationState = aTypingNotificationState(), @@ -145,6 +139,18 @@ fun aMessagesState( 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( target: ReactionSummaryState.Summary? = null, eventSink: (ReactionSummaryEvents) -> Unit = {} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 24439c0c75..0a868fac12 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -154,10 +154,7 @@ fun MessagesView( state.actionListState.eventSink( ActionListEvents.ComputeForMessage( event = event, - canRedactOwn = state.userHasPermissionToRedactOwn, - canRedactOther = state.userHasPermissionToRedactOther, - canSendMessage = state.userHasPermissionToSendMessage, - canSendReaction = state.userHasPermissionToSendReaction, + userEventPermissions = state.userEventPermissions, ) ) } @@ -408,7 +405,7 @@ private fun MessagesViewComposerBottomSheetContents( subcomposing: Boolean, state: MessagesState, ) { - if (state.userHasPermissionToSendMessage) { + if (state.userEventPermissions.canSendMessage) { Column(modifier = Modifier.fillMaxWidth()) { MentionSuggestionsPickerView( modifier = Modifier diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/UserEventPermissions.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/UserEventPermissions.kt new file mode 100644 index 0000000000..84b60ae228 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/UserEventPermissions.kt @@ -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, +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListEvents.kt index e486c1ae2b..407d18afb7 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListEvents.kt @@ -16,15 +16,13 @@ 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 sealed interface ActionListEvents { data object Clear : ActionListEvents data class ComputeForMessage( val event: TimelineItem.Event, - val canRedactOwn: Boolean, - val canRedactOther: Boolean, - val canSendMessage: Boolean, - val canSendReaction: Boolean, + val userEventPermissions: UserEventPermissions, ) : ActionListEvents } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt index d1a19b600d..0f4379cffb 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt @@ -23,15 +23,17 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember 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.timeline.model.TimelineItem 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.TimelineItemPollContent 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.TimelineItemVoiceContent 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.libraries.architecture.Presenter import io.element.android.libraries.preferences.api.store.AppPreferencesStore @@ -58,10 +60,7 @@ class ActionListPresenter @Inject constructor( ActionListEvents.Clear -> target.value = ActionListState.Target.None is ActionListEvents.ComputeForMessage -> localCoroutineScope.computeForMessage( timelineItem = event.event, - userCanRedactOwn = event.canRedactOwn, - userCanRedactOther = event.canRedactOther, - userCanSendMessage = event.canSendMessage, - userCanSendReaction = event.canSendReaction, + usersEventPermissions = event.userEventPermissions, isDeveloperModeEnabled = isDeveloperModeEnabled, target = target, ) @@ -76,136 +75,18 @@ class ActionListPresenter @Inject constructor( private fun CoroutineScope.computeForMessage( timelineItem: TimelineItem.Event, - userCanRedactOwn: Boolean, - userCanRedactOther: Boolean, - userCanSendMessage: Boolean, - userCanSendReaction: Boolean, + usersEventPermissions: UserEventPermissions, isDeveloperModeEnabled: Boolean, target: MutableState ) = launch { target.value = ActionListState.Target.Loading(timelineItem) - val canRedact = timelineItem.isMine && userCanRedactOwn || !timelineItem.isMine && userCanRedactOther - val actions = - when (timelineItem.content) { - is TimelineItemCallNotifyContent -> { - if (isDeveloperModeEnabled) { - listOf(TimelineItemAction.ViewSource) - } else { - 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 { - 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 && + + val actions = buildActions( + timelineItem = timelineItem, + usersEventPermissions = usersEventPermissions, + isDeveloperModeEnabled = isDeveloperModeEnabled, + ) + val displayEmojiReactions = usersEventPermissions.canSendReaction && timelineItem.isRemote && timelineItem.content.canReact() if (actions.isNotEmpty() || displayEmojiReactions) { @@ -219,3 +100,61 @@ class ActionListPresenter @Inject constructor( } } } + +private fun buildActions( + timelineItem: TimelineItem.Event, + usersEventPermissions: UserEventPermissions, + isDeveloperModeEnabled: Boolean, +): List { + 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.postFilter(content: TimelineItemEventContent): List { + return filter { action -> + when (content) { + is TimelineItemCallNotifyContent, + is TimelineItemLegacyCallInviteContent, + is TimelineItemStateContent, + is TimelineItemRedactedContent -> { + action == TimelineItemAction.ViewSource + } + else -> true + } + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt index f6b58e2799..55a82fa791 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt @@ -127,6 +127,7 @@ internal fun aTimelineItemEvent( transactionId: TransactionId? = null, isMine: Boolean = false, isEditable: Boolean = false, + canBeRepliedTo: Boolean = false, senderDisplayName: String = "Sender", displayNameAmbiguous: Boolean = false, content: TimelineItemEventContent = aTimelineItemTextContent(), @@ -150,6 +151,7 @@ internal fun aTimelineItemEvent( sentTime = "12:34", isMine = isMine, isEditable = isEditable, + canBeRepliedTo = canBeRepliedTo, senderProfile = aProfileTimelineDetailsReady( displayName = senderDisplayName, displayNameAmbiguous = displayNameAmbiguous, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt index a2ac0ac7b1..528217d367 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt @@ -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.aTimelineItemImageContent 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.components.EqualWidthColumn import io.element.android.libraries.designsystem.components.avatar.Avatar @@ -148,7 +147,7 @@ fun TimelineItemEventRow( } else { Spacer(modifier = Modifier.height(2.dp)) } - val canReply = timelineRoomInfo.userHasPermissionToSendMessage && event.content.canBeRepliedTo() + val canReply = timelineRoomInfo.userHasPermissionToSendMessage && event.canBeRepliedTo if (canReply) { val state: SwipeableActionsState = rememberSwipeableActionsState() val offset = state.offset.floatValue diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt index c5d303b3f1..3992c3304e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt @@ -76,6 +76,7 @@ class TimelineItemEventFactory @Inject constructor( content = contentFactory.create(currentTimelineItem.event), isMine = currentTimelineItem.event.isOwn, isEditable = currentTimelineItem.event.isEditable, + canBeRepliedTo = currentTimelineItem.event.canBeRepliedTo, sentTime = sentTime, groupPosition = groupPosition, reactionsState = currentTimelineItem.computeReactionsState(), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt index f77db70506..2ebb1bac24 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt @@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.timeline.model 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.TimelineItemRedactedContent 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.virtual.TimelineItemVirtualModel @@ -74,6 +75,7 @@ sealed interface TimelineItem { val sentTime: String = "", val isMine: Boolean = false, val isEditable: Boolean, + val canBeRepliedTo: Boolean, val groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None, val reactionsState: TimelineItemReactions, val readReceiptState: TimelineItemReadReceipts, @@ -94,6 +96,7 @@ sealed interface TimelineItem { val isSticker: Boolean = content is TimelineItemStickerContent val isRemote = eventId != null + } @Immutable diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt index 43fb947433..5a52f476f1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt @@ -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 = - when (this) { - is TimelineItemTextBasedContent, - is TimelineItemStateContent, - is TimelineItemRedactedContent -> true - else -> false - } + this is TimelineItemTextBasedContent /** - * Determine if the event content can be replied to. - * Note: it should match the logic in [io.element.android.features.messages.impl.actionlist.ActionListPresenter]. + * Returns true if the event content can be forwarded. */ -fun TimelineItemEventContent.canBeRepliedTo(): Boolean = +fun TimelineItemEventContent.canBeForwarded(): Boolean = when (this) { - is TimelineItemRedactedContent, - is TimelineItemLegacyCallInviteContent, - is TimelineItemCallNotifyContent, - is TimelineItemStateContent -> false - else -> true + is TimelineItemTextBasedContent, + is TimelineItemImageContent, + is TimelineItemFileContent, + is TimelineItemAudioContent, + 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 } /** diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index 091305893e..6b06adeb03 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -137,8 +137,8 @@ class MessagesPresenterTest { assertThat(initialState.roomName).isEqualTo(AsyncData.Success("")) assertThat(initialState.roomAvatar) .isEqualTo(AsyncData.Success(AvatarData(id = A_ROOM_ID.value, name = "", url = AN_AVATAR_URL, size = AvatarSize.TimelineRoom))) - assertThat(initialState.userHasPermissionToSendMessage).isTrue() - assertThat(initialState.userHasPermissionToRedactOwn).isTrue() + assertThat(initialState.userEventPermissions.canSendMessage).isTrue() + assertThat(initialState.userEventPermissions.canRedactOwn).isTrue() assertThat(initialState.hasNetworkConnection).isTrue() assertThat(initialState.snackbarMessage).isNull() assertThat(initialState.inviteProgress).isEqualTo(AsyncData.Uninitialized) @@ -787,7 +787,7 @@ class MessagesPresenterTest { presenter.present() }.test { val state = awaitFirstItem() - assertThat(state.userHasPermissionToSendMessage).isTrue() + assertThat(state.userEventPermissions.canSendMessage).isTrue() } } @@ -811,9 +811,9 @@ class MessagesPresenterTest { presenter.present() }.test { // Default value - assertThat(awaitItem().userHasPermissionToSendMessage).isTrue() + assertThat(awaitItem().userEventPermissions.canSendMessage).isTrue() skipItems(1) - assertThat(awaitItem().userHasPermissionToSendMessage).isFalse() + assertThat(awaitItem().userEventPermissions.canSendMessage).isFalse() cancelAndIgnoreRemainingEvents() } } @@ -831,9 +831,9 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = consumeItemsUntilPredicate { it.userHasPermissionToRedactOwn }.last() - assertThat(initialState.userHasPermissionToRedactOwn).isTrue() - assertThat(initialState.userHasPermissionToRedactOther).isFalse() + val initialState = consumeItemsUntilPredicate { it.userEventPermissions.canRedactOwn }.last() + assertThat(initialState.userEventPermissions.canRedactOwn).isTrue() + assertThat(initialState.userEventPermissions.canRedactOther).isFalse() cancelAndIgnoreRemainingEvents() } } @@ -851,9 +851,9 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = consumeItemsUntilPredicate { it.userHasPermissionToRedactOther }.last() - assertThat(initialState.userHasPermissionToRedactOwn).isFalse() - assertThat(initialState.userHasPermissionToRedactOther).isTrue() + val initialState = consumeItemsUntilPredicate { it.userEventPermissions.canRedactOther }.last() + assertThat(initialState.userEventPermissions.canRedactOwn).isFalse() + assertThat(initialState.userEventPermissions.canRedactOther).isTrue() cancelAndIgnoreRemainingEvents() } } @@ -963,6 +963,7 @@ class MessagesPresenterTest { room = matrixRoom, sessionPreferencesStore = sessionPreferencesStore, ) + val readReceiptBottomSheetPresenter = ReadReceiptBottomSheetPresenter() val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider()) val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt index 9a22ced505..446c36f74c 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.test.onAllNodesWithContentDescription import androidx.compose.ui.test.onAllNodesWithTag import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onFirst +import androidx.compose.ui.test.onLast import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithTag 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.messagecomposer.aMessageComposerState 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.aTimelineRoomInfo import io.element.android.features.messages.impl.timeline.aTimelineState @@ -175,10 +175,12 @@ class MessagesViewTest { actionListState = anActionListState( eventSink = eventsRecorder ), - userHasPermissionToSendMessage = userHasPermissionToSendMessage, - userHasPermissionToRedactOwn = userHasPermissionToRedactOwn, - userHasPermissionToRedactOther = userHasPermissionToRedactOther, - userHasPermissionToSendReaction = userHasPermissionToSendReaction, + userEventPermissions = UserEventPermissions( + canSendMessage = userHasPermissionToSendMessage, + canRedactOwn = userHasPermissionToRedactOwn, + canRedactOther = userHasPermissionToRedactOther, + canSendReaction = userHasPermissionToSendReaction, + ), ) val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event rule.setMessagesView( @@ -189,10 +191,7 @@ class MessagesViewTest { eventsRecorder.assertSingle( ActionListEvents.ComputeForMessage( event = timelineItem, - canRedactOwn = state.userHasPermissionToRedactOwn, - canRedactOther = state.userHasPermissionToRedactOther, - canSendMessage = state.userHasPermissionToSendMessage, - canSendReaction = state.userHasPermissionToSendReaction, + userEventPermissions = state.userEventPermissions, ) ) } @@ -237,9 +236,11 @@ class MessagesViewTest { private fun swipeTest(userHasPermissionToSendMessage: Boolean) { val eventsRecorder = EventsRecorder() + val canBeRepliedEvent = aTimelineItemEvent(canBeRepliedTo = true) + val cannotBeRepliedEvent = aTimelineItemEvent(canBeRepliedTo = false) val state = aMessagesState( timelineState = aTimelineState( - timelineItems = aTimelineItemList(aTimelineItemTextContent()), + timelineItems = persistentListOf(canBeRepliedEvent, cannotBeRepliedEvent), timelineRoomInfo = aTimelineRoomInfo( userHasPermissionToSendMessage = userHasPermissionToSendMessage ), @@ -249,10 +250,12 @@ class MessagesViewTest { rule.setMessagesView( 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) { - val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event - eventsRecorder.assertSingle(MessagesEvents.HandleAction(TimelineItemAction.Reply, timelineItem)) + eventsRecorder.assertSingle(MessagesEvents.HandleAction(TimelineItemAction.Reply, canBeRepliedEvent)) } else { eventsRecorder.assertEmpty() } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt index 76c43a9c8b..716c133c0a 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt @@ -20,6 +20,7 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test 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.fixtures.aMessageEvent 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.aTimelineItemVoiceContent 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.preferences.test.InMemoryAppPreferencesStore import io.element.android.tests.testutils.WarmUpRule @@ -66,10 +68,12 @@ class ActionListPresenterTest { initialState.eventSink.invoke( ActionListEvents.ComputeForMessage( event = messageEvent, - canRedactOwn = false, - canRedactOther = false, - canSendMessage = true, - canSendReaction = true, + userEventPermissions = aUserEventPermissions( + canRedactOwn = false, + canRedactOther = false, + canSendMessage = true, + canSendReaction = true, + ) ) ) // val loadingState = awaitItem() @@ -104,10 +108,12 @@ class ActionListPresenterTest { initialState.eventSink.invoke( ActionListEvents.ComputeForMessage( event = messageEvent, - canRedactOwn = false, - canRedactOther = false, - canSendMessage = true, - canSendReaction = true, + userEventPermissions = aUserEventPermissions( + canRedactOwn = false, + canRedactOther = false, + canSendMessage = true, + canSendReaction = true, + ) ) ) // val loadingState = awaitItem() @@ -142,10 +148,12 @@ class ActionListPresenterTest { initialState.eventSink.invoke( ActionListEvents.ComputeForMessage( event = messageEvent, - canRedactOwn = false, - canRedactOther = false, - canSendMessage = true, - canSendReaction = true, + userEventPermissions = aUserEventPermissions( + canRedactOwn = false, + canRedactOther = false, + canSendMessage = true, + canSendReaction = true, + ) ) ) // val loadingState = awaitItem() @@ -185,10 +193,12 @@ class ActionListPresenterTest { initialState.eventSink.invoke( ActionListEvents.ComputeForMessage( event = messageEvent, - canRedactOwn = true, - canRedactOther = false, - canSendMessage = false, - canSendReaction = true + userEventPermissions = aUserEventPermissions( + canRedactOwn = true, + canRedactOther = false, + canSendMessage = false, + canSendReaction = true + ) ) ) // val loadingState = awaitItem() @@ -227,10 +237,12 @@ class ActionListPresenterTest { initialState.eventSink.invoke( ActionListEvents.ComputeForMessage( event = messageEvent, - canRedactOwn = false, - canRedactOther = true, - canSendMessage = true, - canSendReaction = true, + userEventPermissions = aUserEventPermissions( + canRedactOwn = false, + canRedactOther = true, + canSendMessage = true, + canSendReaction = true, + ) ) ) val successState = awaitItem() @@ -269,10 +281,12 @@ class ActionListPresenterTest { initialState.eventSink.invoke( ActionListEvents.ComputeForMessage( event = messageEvent, - canRedactOwn = false, - canRedactOther = true, - canSendMessage = true, - canSendReaction = false + userEventPermissions = aUserEventPermissions( + canRedactOwn = false, + canRedactOther = true, + canSendMessage = true, + canSendReaction = false + ) ) ) val successState = awaitItem() @@ -310,10 +324,12 @@ class ActionListPresenterTest { initialState.eventSink.invoke( ActionListEvents.ComputeForMessage( event = messageEvent, - canRedactOwn = true, - canRedactOther = false, - canSendMessage = true, - canSendReaction = true, + userEventPermissions = aUserEventPermissions( + canRedactOwn = true, + canRedactOther = false, + canSendMessage = true, + canSendReaction = true, + ) ) ) // val loadingState = awaitItem() @@ -353,10 +369,12 @@ class ActionListPresenterTest { initialState.eventSink.invoke( ActionListEvents.ComputeForMessage( event = messageEvent, - canRedactOwn = false, - canRedactOther = false, - canSendMessage = true, - canSendReaction = true, + userEventPermissions = aUserEventPermissions( + canRedactOwn = false, + canRedactOther = false, + canSendMessage = true, + canSendReaction = true, + ) ) ) // val loadingState = awaitItem() @@ -396,10 +414,12 @@ class ActionListPresenterTest { initialState.eventSink.invoke( ActionListEvents.ComputeForMessage( event = messageEvent, - canRedactOwn = true, - canRedactOther = false, - canSendMessage = true, - canSendReaction = true, + userEventPermissions = aUserEventPermissions( + canRedactOwn = true, + canRedactOther = false, + canSendMessage = true, + canSendReaction = true, + ), ) ) // val loadingState = awaitItem() @@ -437,10 +457,12 @@ class ActionListPresenterTest { initialState.eventSink.invoke( ActionListEvents.ComputeForMessage( event = stateEvent, - canRedactOwn = false, - canRedactOther = false, - canSendMessage = true, - canSendReaction = true, + userEventPermissions = aUserEventPermissions( + canRedactOwn = false, + canRedactOther = false, + canSendMessage = true, + canSendReaction = true, + ) ) ) // val loadingState = awaitItem() @@ -451,8 +473,6 @@ class ActionListPresenterTest { event = stateEvent, displayEmojiReactions = false, actions = persistentListOf( - TimelineItemAction.Copy, - TimelineItemAction.CopyLink, TimelineItemAction.ViewSource, ) ) @@ -476,26 +496,16 @@ class ActionListPresenterTest { initialState.eventSink.invoke( ActionListEvents.ComputeForMessage( event = stateEvent, - canRedactOwn = false, - canRedactOther = false, - canSendMessage = true, - canSendReaction = true, + userEventPermissions = aUserEventPermissions( + canRedactOwn = false, + canRedactOther = false, + canSendMessage = true, + canSendReaction = true, + ) ) ) // val loadingState = awaitItem() // 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) } } @@ -514,10 +524,12 @@ class ActionListPresenterTest { initialState.eventSink.invoke( ActionListEvents.ComputeForMessage( event = messageEvent, - canRedactOwn = true, - canRedactOther = false, - canSendMessage = true, - canSendReaction = true, + userEventPermissions = aUserEventPermissions( + canRedactOwn = true, + canRedactOther = false, + canSendMessage = true, + canSendReaction = true, + ) ) ) // val loadingState = awaitItem() @@ -561,10 +573,12 @@ class ActionListPresenterTest { initialState.eventSink.invoke( ActionListEvents.ComputeForMessage( event = messageEvent, - canRedactOwn = false, - canRedactOther = false, - canSendMessage = true, - canSendReaction = true, + userEventPermissions = aUserEventPermissions( + canRedactOwn = false, + canRedactOther = false, + canSendMessage = true, + canSendReaction = true, + ) ) ) assertThat(awaitItem().target).isInstanceOf(ActionListState.Target.Success::class.java) @@ -572,10 +586,12 @@ class ActionListPresenterTest { initialState.eventSink.invoke( ActionListEvents.ComputeForMessage( event = redactedEvent, - canRedactOwn = false, - canRedactOther = false, - canSendMessage = true, - canSendReaction = true, + userEventPermissions = aUserEventPermissions( + canRedactOwn = false, + canRedactOther = false, + canSendMessage = true, + canSendReaction = true, + ) ) ) awaitItem().run { @@ -595,16 +611,19 @@ class ActionListPresenterTest { // No event id, so it's not sent yet eventId = null, isMine = true, + canBeRepliedTo = false, content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null), ) initialState.eventSink.invoke( ActionListEvents.ComputeForMessage( event = messageEvent, - canRedactOwn = true, - canRedactOther = false, - canSendMessage = true, - canSendReaction = true, + userEventPermissions = aUserEventPermissions( + canRedactOwn = true, + canRedactOther = false, + canSendMessage = true, + canSendReaction = true, + ) ) ) val successState = awaitItem() @@ -637,10 +656,12 @@ class ActionListPresenterTest { initialState.eventSink.invoke( ActionListEvents.ComputeForMessage( event = messageEvent, - canRedactOwn = true, - canRedactOther = false, - canSendMessage = true, - canSendReaction = true, + userEventPermissions = aUserEventPermissions( + canRedactOwn = true, + canRedactOther = false, + canSendMessage = true, + canSendReaction = true, + ) ) ) val successState = awaitItem() @@ -675,10 +696,12 @@ class ActionListPresenterTest { initialState.eventSink.invoke( ActionListEvents.ComputeForMessage( event = messageEvent, - canRedactOwn = true, - canRedactOther = false, - canSendMessage = true, - canSendReaction = true, + userEventPermissions = aUserEventPermissions( + canRedactOwn = true, + canRedactOther = false, + canSendMessage = true, + canSendReaction = true, + ) ) ) val successState = awaitItem() @@ -712,10 +735,12 @@ class ActionListPresenterTest { initialState.eventSink.invoke( ActionListEvents.ComputeForMessage( event = messageEvent, - canRedactOwn = true, - canRedactOther = false, - canSendMessage = true, - canSendReaction = true, + userEventPermissions = aUserEventPermissions( + canRedactOwn = true, + canRedactOther = false, + canSendMessage = true, + canSendReaction = true, + ) ) ) val successState = awaitItem() @@ -742,15 +767,18 @@ class ActionListPresenterTest { val initialState = awaitItem() val messageEvent = aMessageEvent( isMine = true, + isEditable = false, content = aTimelineItemVoiceContent(), ) initialState.eventSink.invoke( ActionListEvents.ComputeForMessage( event = messageEvent, - canRedactOwn = true, - canRedactOther = false, - canSendMessage = true, - canSendReaction = true, + userEventPermissions = aUserEventPermissions( + canRedactOwn = true, + canRedactOther = false, + canSendMessage = true, + canSendReaction = true, + ) ) ) val successState = awaitItem() @@ -783,10 +811,12 @@ class ActionListPresenterTest { initialState.eventSink.invoke( ActionListEvents.ComputeForMessage( event = messageEvent, - canRedactOwn = true, - canRedactOther = false, - canSendMessage = true, - canSendReaction = true, + userEventPermissions = aUserEventPermissions( + canRedactOwn = true, + canRedactOther = false, + canSendMessage = true, + canSendReaction = true, + ) ) ) val successState = awaitItem() diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/MessageEventFixtures.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/MessageEventFixtures.kt index 7f2c3cfbeb..ca805d44c7 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/MessageEventFixtures.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/MessageEventFixtures.kt @@ -42,6 +42,7 @@ internal fun aMessageEvent( transactionId: TransactionId? = null, isMine: Boolean = true, isEditable: Boolean = true, + canBeRepliedTo: Boolean = true, content: TimelineItemEventContent = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, formattedBody = null, isEdited = false), inReplyTo: InReplyToDetails? = null, isThreaded: Boolean = false, @@ -58,6 +59,7 @@ internal fun aMessageEvent( sentTime = "", isMine = isMine, isEditable = isEditable, + canBeRepliedTo = canBeRepliedTo, reactionsState = aTimelineItemReactions(count = 0), readReceiptState = TimelineItemReadReceipts(emptyList().toImmutableList()), localSendState = sendState, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt index ce53e05090..e044ac7e2b 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt @@ -46,6 +46,7 @@ class TimelineItemGrouperTest { readReceiptState = TimelineItemReadReceipts(emptyList().toImmutableList()), localSendState = LocalEventSendState.Sent(AN_EVENT_ID), isEditable = false, + canBeRepliedTo = false, inReplyTo = null, isThreaded = false, debugInfo = aTimelineItemDebugInfo(), diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/RedactedVoiceMessageManagerTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/RedactedVoiceMessageManagerTest.kt index 4cabc6450a..2a54edad58 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/RedactedVoiceMessageManagerTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/RedactedVoiceMessageManagerTest.kt @@ -85,6 +85,7 @@ fun aRedactedMatrixTimeline(eventId: EventId) = listOf( eventId = eventId, transactionId = null, isEditable = false, + canBeRepliedTo = false, isLocal = false, isOwn = false, isRemote = false, diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt index fa15f8f096..4a8f330c5c 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt @@ -26,6 +26,7 @@ data class EventTimelineItem( val eventId: EventId?, val transactionId: TransactionId?, val isEditable: Boolean, + val canBeRepliedTo: Boolean, val isLocal: Boolean, val isOwn: Boolean, val isRemote: Boolean, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt index dd0bdabd7f..2ae0a3e909 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt @@ -44,6 +44,7 @@ class EventTimelineItemMapper(private val contentMapper: TimelineEventContentMap eventId = it.eventId()?.let(::EventId), transactionId = it.transactionId()?.let(::TransactionId), isEditable = it.isEditable(), + canBeRepliedTo = it.canBeRepliedTo(), isLocal = it.isLocal(), isOwn = it.isOwn(), isRemote = it.isRemote(), diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/TimelineFixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/TimelineFixture.kt index d53bc7b18f..f9e9efb210 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/TimelineFixture.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/TimelineFixture.kt @@ -46,6 +46,7 @@ fun anEventTimelineItem( eventId: EventId = AN_EVENT_ID, transactionId: TransactionId? = null, isEditable: Boolean = false, + canBeRepliedTo: Boolean = false, isLocal: Boolean = false, isOwn: Boolean = false, isRemote: Boolean = false, @@ -61,6 +62,7 @@ fun anEventTimelineItem( eventId = eventId, transactionId = transactionId, isEditable = isEditable, + canBeRepliedTo = canBeRepliedTo, isLocal = isLocal, isOwn = isOwn, isRemote = isRemote,