diff --git a/changelog.d/712.bugfix b/changelog.d/712.bugfix new file mode 100644 index 0000000000..7a3115a610 --- /dev/null +++ b/changelog.d/712.bugfix @@ -0,0 +1 @@ +Fix actions for redacted, not sent and media messages diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index ef9db5fcec..1593c19afc 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -90,7 +90,7 @@ class MessagesFlowNode @AssistedInject constructor( data class LocationViewer(val location: Location, val description: String?) : NavTarget @Parcelize - data class EventDebugInfo(val eventId: EventId, val debugInfo: TimelineItemDebugInfo) : NavTarget + data class EventDebugInfo(val eventId: EventId?, val debugInfo: TimelineItemDebugInfo) : NavTarget @Parcelize data class ForwardEvent(val eventId: EventId) : NavTarget @@ -124,7 +124,7 @@ class MessagesFlowNode @AssistedInject constructor( callback?.onUserDataClicked(userId) } - override fun onShowEventDebugInfoClicked(eventId: EventId, debugInfo: TimelineItemDebugInfo) { + override fun onShowEventDebugInfoClicked(eventId: EventId?, debugInfo: TimelineItemDebugInfo) { backstack.push(NavTarget.EventDebugInfo(eventId, debugInfo)) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNavigator.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNavigator.kt index 201173a0bf..a0517c59c4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNavigator.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNavigator.kt @@ -21,7 +21,7 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo interface MessagesNavigator { - fun onShowEventDebugInfoClicked(eventId: EventId, debugInfo: TimelineItemDebugInfo) + fun onShowEventDebugInfoClicked(eventId: EventId?, debugInfo: TimelineItemDebugInfo) fun onForwardEventClicked(eventId: EventId) fun onReportContentClicked(eventId: EventId, senderId: UserId) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt index 8a44d0b748..3f201a8e4c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt @@ -54,7 +54,7 @@ class MessagesNode @AssistedInject constructor( fun onEventClicked(event: TimelineItem.Event) fun onPreviewAttachments(attachments: ImmutableList) fun onUserDataClicked(userId: UserId) - fun onShowEventDebugInfoClicked(eventId: EventId, debugInfo: TimelineItemDebugInfo) + fun onShowEventDebugInfoClicked(eventId: EventId?, debugInfo: TimelineItemDebugInfo) fun onForwardEventClicked(eventId: EventId) fun onReportMessage(eventId: EventId, senderId: UserId) fun onSendLocationClicked() @@ -83,7 +83,7 @@ class MessagesNode @AssistedInject constructor( private fun onUserDataClicked(userId: UserId) { callback?.onUserDataClicked(userId) } - override fun onShowEventDebugInfoClicked(eventId: EventId, debugInfo: TimelineItemDebugInfo) { + override fun onShowEventDebugInfoClicked(eventId: EventId?, debugInfo: TimelineItemDebugInfo) { callback?.onShowEventDebugInfoClicked(eventId, debugInfo) } @@ -94,7 +94,7 @@ class MessagesNode @AssistedInject constructor( override fun onReportContentClicked(eventId: EventId, senderId: UserId) { callback?.onReportMessage(eventId, senderId) } - + private fun onSendLocationClicked() { callback?.onSendLocationClicked() } 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 f2c8deca49..4867749a2b 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 @@ -227,15 +227,19 @@ class MessagesPresenter @AssistedInject constructor( } private suspend fun handleActionRedact(event: TimelineItem.Event) { - if (event.eventId == null) return - room.redactEvent(event.eventId) + if (event.failedToSend) { + // If the message hasn't been sent yet, just cancel it + event.transactionId?.let { room.cancelSend(it) } + } else if (event.eventId != null) { + room.redactEvent(event.eventId) + } } private fun handleActionEdit(targetEvent: TimelineItem.Event, composerState: MessageComposerState) { - if (targetEvent.eventId == null) return val composerMode = MessageComposerMode.Edit( targetEvent.eventId, - (targetEvent.content as? TimelineItemTextBasedContent)?.body.orEmpty() + (targetEvent.content as? TimelineItemTextBasedContent)?.body.orEmpty(), + targetEvent.transactionId, ) composerState.eventSink( MessageComposerEvents.SetMode(composerMode) @@ -288,7 +292,6 @@ class MessagesPresenter @AssistedInject constructor( } private fun handleShowDebugInfoAction(event: TimelineItem.Event) { - if (event.eventId == null) return navigator.onShowEventDebugInfoClicked(event.eventId, event.debugInfo) } 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 26ab4fc217..e8fee402e1 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 @@ -18,6 +18,8 @@ package io.element.android.features.messages.impl.actionlist import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -28,6 +30,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.canBeCopied import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -45,6 +48,10 @@ class ActionListPresenter @Inject constructor( mutableStateOf(ActionListState.Target.None) } + val displayEmojiReactions by remember { + derivedStateOf { (target.value as? ActionListState.Target.Success)?.event?.sendState is EventSendState.Sent } + } + fun handleEvents(event: ActionListEvents) { when (event) { ActionListEvents.Clear -> target.value = ActionListState.Target.None @@ -54,29 +61,37 @@ class ActionListPresenter @Inject constructor( return ActionListState( target = target.value, + displayEmojiReactions = displayEmojiReactions, eventSink = ::handleEvents ) } private fun CoroutineScope.computeForMessage(timelineItem: TimelineItem.Event, target: MutableState) = launch { target.value = ActionListState.Target.Loading(timelineItem) + val itemSent = timelineItem.sendState is EventSendState.Sent val actions = when (timelineItem.content) { - is TimelineItemRedactedContent, + is TimelineItemRedactedContent -> { + if (buildMeta.isDebuggable) { + listOf(TimelineItemAction.Developer) + } else { + emptyList() + } + } is TimelineItemStateContent -> { buildList { - if (timelineItem.content.canBeCopied()) { - add(TimelineItemAction.Copy) - } + add(TimelineItemAction.Copy) if (buildMeta.isDebuggable) { add(TimelineItemAction.Developer) } } } else -> buildList { - add(TimelineItemAction.Reply) - add(TimelineItemAction.Forward) - if (timelineItem.isMine) { + if (itemSent) { + add(TimelineItemAction.Reply) + add(TimelineItemAction.Forward) + } + if (timelineItem.isMine && timelineItem.isTextMessage) { add(TimelineItemAction.Edit) } if (timelineItem.content.canBeCopied()) { @@ -93,6 +108,10 @@ class ActionListPresenter @Inject constructor( } } } - target.value = ActionListState.Target.Success(timelineItem, actions.toImmutableList()) + if (actions.isNotEmpty()) { + target.value = ActionListState.Target.Success(timelineItem, actions.toImmutableList()) + } else { + target.value = ActionListState.Target.None + } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListState.kt index faf41160be..aac3469218 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListState.kt @@ -24,6 +24,7 @@ import kotlinx.collections.immutable.ImmutableList @Immutable data class ActionListState( val target: Target, + val displayEmojiReactions: Boolean, val eventSink: (ActionListEvents) -> Unit, ) { sealed interface Target { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt index ee1b7de309..01107d4308 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt @@ -61,11 +61,19 @@ open class ActionListStateProvider : PreviewParameterProvider { actions = aTimelineItemActionList(), ) ), + anActionListState().copy( + target = ActionListState.Target.Success( + event = aTimelineItemEvent(content = aTimelineItemLocationContent()), + actions = aTimelineItemActionList(), + ), + displayEmojiReactions = false, + ), ) } fun anActionListState() = ActionListState( target = ActionListState.Target.None, + displayEmojiReactions = true, eventSink = {} ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt index 904313120f..ca1b07183a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt @@ -175,13 +175,15 @@ private fun SheetContent( Divider() } } - item { - EmojiReactionsRow( - onEmojiReactionClicked = onEmojiReactionClicked, - onCustomReactionClicked = onCustomReactionClicked, - modifier = Modifier.fillMaxWidth(), - ) - Divider() + if (state.displayEmojiReactions) { + item { + EmojiReactionsRow( + onEmojiReactionClicked = onEmojiReactionClicked, + onCustomReactionClicked = onCustomReactionClicked, + modifier = Modifier.fillMaxWidth(), + ) + Divider() + } } items( items = actions, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index 16c86f9baa..3ad2c497ce 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -196,10 +196,11 @@ class MessageComposerPresenter @Inject constructor( composerMode.setToNormal() when (capturedMode) { is MessageComposerMode.Normal -> room.sendMessage(text) - is MessageComposerMode.Edit -> room.editMessage( - capturedMode.eventId, - text - ) + is MessageComposerMode.Edit -> { + val eventId = capturedMode.eventId + val transactionId = capturedMode.transactionId + room.editMessage(eventId, transactionId, text) + } is MessageComposerMode.Quote -> TODO() is MessageComposerMode.Reply -> room.replyMessage( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index ab1adb97ae..c632cf1b01 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -161,28 +161,20 @@ fun TimelineItemRow( ) } is TimelineItem.Event -> { - fun onClick() { - onClick(timelineItem) - } - - fun onLongClick() { - onLongClick(timelineItem) - } - if (timelineItem.content is TimelineItemStateContent) { TimelineItemStateEventRow( event = timelineItem, isHighlighted = highlightedItem == timelineItem.identifier(), - onClick = ::onClick, - onLongClick = ::onLongClick, + onClick = { onClick(timelineItem) }, + onLongClick = { onLongClick(timelineItem) }, modifier = modifier, ) } else { TimelineItemEventRow( event = timelineItem, isHighlighted = highlightedItem == timelineItem.identifier(), - onClick = ::onClick, - onLongClick = ::onLongClick, + onClick = { onClick(timelineItem) }, + onLongClick = { onLongClick(timelineItem) }, onUserDataClick = onUserDataClick, inReplyToClick = inReplyToClick, onReactionClick = onReactionClick, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/debug/EventDebugInfoNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/debug/EventDebugInfoNode.kt index faa80d134b..b9e4a75d97 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/debug/EventDebugInfoNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/debug/EventDebugInfoNode.kt @@ -37,7 +37,7 @@ class EventDebugInfoNode @AssistedInject constructor( ) : Node(buildContext, plugins = plugins) { data class Inputs( - val eventId: EventId, + val eventId: EventId?, val timelineItemDebugInfo: TimelineItemDebugInfo, ) : NodeInputs diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/debug/EventDebugInfoView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/debug/EventDebugInfoView.kt index f3fc79e0a3..e6ecab9d5a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/debug/EventDebugInfoView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/debug/EventDebugInfoView.kt @@ -70,7 +70,7 @@ import io.element.android.libraries.matrix.api.core.EventId @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @Composable fun EventDebugInfoView( - eventId: EventId, + eventId: EventId?, model: String, originalJson: String?, latestEditedJson: String?, @@ -99,7 +99,7 @@ fun EventDebugInfoView( item { Column(Modifier.padding(vertical = 10.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) { Text(text = "Event ID:") - CopyableText(text = eventId.value) + CopyableText(text = eventId?.value ?: "-", modifier = Modifier.fillMaxWidth()) } } item { @@ -142,7 +142,7 @@ private fun CollapsibleSection( ) } AnimatedVisibility(visible = isExpanded, enter = expandVertically(), exit = shrinkVertically()) { - CopyableText(text = text) + CopyableText(text = text, modifier = Modifier.fillMaxWidth()) } } } 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 08f9df0535..9e830d867f 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.TimelineItemTextBasedContent import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.matrix.api.core.EventId @@ -69,6 +70,10 @@ sealed interface TimelineItem { val showSenderInformation = groupPosition.isNew() && !isMine val safeSenderName: String = senderDisplayName ?: senderId.value + + val failedToSend: Boolean = sendState is EventSendState.SendingFailed + + val isTextMessage: Boolean = content is TimelineItemTextBasedContent } @Immutable diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/FakeMessagesNavigator.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/FakeMessagesNavigator.kt index 8a374e5bcb..bb2caa9405 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/FakeMessagesNavigator.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/FakeMessagesNavigator.kt @@ -31,7 +31,7 @@ class FakeMessagesNavigator : MessagesNavigator { var onReportContentClickedCount = 0 private set - override fun onShowEventDebugInfoClicked(eventId: EventId, debugInfo: TimelineItemDebugInfo) { + override fun onShowEventDebugInfoClicked(eventId: EventId?, debugInfo: TimelineItemDebugInfo) { onShowEventDebugInfoClickedCount++ } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt index c90a03e5bb..f637e08724 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt @@ -25,9 +25,12 @@ import io.element.android.features.messages.impl.actionlist.ActionListEvents import io.element.android.features.messages.impl.actionlist.ActionListPresenter import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction +import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent +import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent +import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState import io.element.android.libraries.matrix.test.A_MESSAGE import io.element.android.libraries.matrix.test.core.aBuildMeta import kotlinx.collections.immutable.persistentListOf @@ -62,7 +65,6 @@ class ActionListPresenterTest { ActionListState.Target.Success( messageEvent, persistentListOf( - TimelineItemAction.Copy, TimelineItemAction.Developer, ) ) @@ -88,7 +90,6 @@ class ActionListPresenterTest { ActionListState.Target.Success( messageEvent, persistentListOf( - TimelineItemAction.Copy, TimelineItemAction.Developer, ) ) @@ -184,7 +185,6 @@ class ActionListPresenterTest { persistentListOf( TimelineItemAction.Reply, TimelineItemAction.Forward, - TimelineItemAction.Edit, TimelineItemAction.Developer, TimelineItemAction.Redact, ) @@ -195,6 +195,63 @@ class ActionListPresenterTest { } } + @Test + fun `present - compute for a state item in debug build`() = runTest { + val presenter = anActionListPresenter(isBuildDebuggable = true) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val stateEvent = aTimelineItemEvent( + isMine = true, + content = aTimelineItemStateEventContent(), + ) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent)) + // val loadingState = awaitItem() + // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) + val successState = awaitItem() + assertThat(successState.target).isEqualTo( + ActionListState.Target.Success( + stateEvent, + persistentListOf( + TimelineItemAction.Copy, + TimelineItemAction.Developer, + ) + ) + ) + initialState.eventSink.invoke(ActionListEvents.Clear) + assertThat(awaitItem().target).isEqualTo(ActionListState.Target.None) + } + } + + @Test + fun `present - compute for a state item in non-debuggable build`() = runTest { + val presenter = anActionListPresenter(isBuildDebuggable = false) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val stateEvent = aTimelineItemEvent( + isMine = true, + content = aTimelineItemStateEventContent(), + ) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent)) + // val loadingState = awaitItem() + // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) + val successState = awaitItem() + assertThat(successState.target).isEqualTo( + ActionListState.Target.Success( + stateEvent, + persistentListOf( + TimelineItemAction.Copy, + ) + ) + ) + initialState.eventSink.invoke(ActionListEvents.Clear) + assertThat(awaitItem().target).isEqualTo(ActionListState.Target.None) + } + } + @Test fun `present - compute message in non-debuggable build`() = runTest { val presenter = anActionListPresenter(isBuildDebuggable = false) @@ -226,6 +283,62 @@ class ActionListPresenterTest { assertThat(awaitItem().target).isEqualTo(ActionListState.Target.None) } } + + @Test + fun `present - compute message with no actions`() = runTest { + val presenter = anActionListPresenter(isBuildDebuggable = false) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val messageEvent = aMessageEvent( + isMine = true, + content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false) + ) + val redactedEvent = aMessageEvent( + isMine = true, + content = TimelineItemRedactedContent, + ) + + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent)) + assertThat(awaitItem().target).isInstanceOf(ActionListState.Target.Success::class.java) + + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(redactedEvent)) + awaitItem().run { + assertThat(target).isEqualTo(ActionListState.Target.None) + assertThat(displayEmojiReactions).isFalse() + } + } + } + + @Test + fun `present - compute not sent message`() = runTest { + val presenter = anActionListPresenter(isBuildDebuggable = false) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val messageEvent = aMessageEvent( + isMine = true, + content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false), + sendState = EventSendState.NotSentYet, + ) + + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent)) + val successState = awaitItem() + assertThat(successState.target).isEqualTo( + ActionListState.Target.Success( + messageEvent, + persistentListOf( + TimelineItemAction.Edit, + TimelineItemAction.Copy, + TimelineItemAction.Redact, + ) + ) + ) + assertThat(successState.displayEmojiReactions).isFalse() + } + } } private fun anActionListPresenter(isBuildDebuggable: Boolean) = ActionListPresenter(buildMeta = aBuildMeta(isDebuggable = isBuildDebuggable)) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/aMessageEvent.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/aMessageEvent.kt index 6298042e89..9a4b7ae38b 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/aMessageEvent.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/aMessageEvent.kt @@ -38,6 +38,7 @@ internal fun aMessageEvent( content: TimelineItemEventContent = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false), inReplyTo: InReplyTo? = null, debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(), + sendState: EventSendState = EventSendState.Sent(AN_EVENT_ID), ) = TimelineItem.Event( id = eventId?.value.orEmpty(), eventId = eventId, @@ -48,7 +49,7 @@ internal fun aMessageEvent( sentTime = "", isMine = isMine, reactionsState = aTimelineItemReactions(count = 0), - sendState = EventSendState.Sent(AN_EVENT_ID), + sendState = sendState, inReplyTo = inReplyTo, debugInfo = debugInfo, ) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt index 8e96820c47..97bbf925bd 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt @@ -36,6 +36,7 @@ import io.element.android.libraries.designsystem.utils.SnackbarDispatcher import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService +import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.media.ImageInfo import io.element.android.libraries.matrix.api.media.VideoInfo import io.element.android.libraries.matrix.api.room.MatrixRoom @@ -43,6 +44,7 @@ import io.element.android.libraries.matrix.test.ANOTHER_MESSAGE import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_MESSAGE import io.element.android.libraries.matrix.test.A_REPLY +import io.element.android.libraries.matrix.test.A_TRANSACTION_ID import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.mediapickers.api.PickerProvider @@ -193,7 +195,7 @@ class MessageComposerPresenterTest { } @Test - fun `present - edit message`() = runTest { + fun `present - edit sent message`() = runTest { val fakeMatrixRoom = FakeMatrixRoom() val presenter = createPresenter( this, @@ -219,7 +221,38 @@ class MessageComposerPresenterTest { val messageSentState = awaitItem() assertThat(messageSentState.text).isEqualTo(StableCharSequence("")) assertThat(messageSentState.isSendButtonVisible).isFalse() - assertThat(fakeMatrixRoom.editMessageParameter).isEqualTo(ANOTHER_MESSAGE) + assertThat(fakeMatrixRoom.editMessageCalls.first()).isEqualTo(ANOTHER_MESSAGE) + } + } + + @Test + fun `present - edit not sent message`() = runTest { + val fakeMatrixRoom = FakeMatrixRoom() + val presenter = createPresenter( + this, + fakeMatrixRoom, + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.text).isEqualTo(StableCharSequence("")) + val mode = anEditMode(eventId = null, transactionId = A_TRANSACTION_ID) + initialState.eventSink.invoke(MessageComposerEvents.SetMode(mode)) + skipItems(1) + val withMessageState = awaitItem() + assertThat(withMessageState.mode).isEqualTo(mode) + assertThat(withMessageState.text).isEqualTo(StableCharSequence(A_MESSAGE)) + assertThat(withMessageState.isSendButtonVisible).isTrue() + withMessageState.eventSink.invoke(MessageComposerEvents.UpdateText(ANOTHER_MESSAGE)) + val withEditedMessageState = awaitItem() + assertThat(withEditedMessageState.text).isEqualTo(StableCharSequence(ANOTHER_MESSAGE)) + withEditedMessageState.eventSink.invoke(MessageComposerEvents.SendMessage(ANOTHER_MESSAGE)) + skipItems(1) + val messageSentState = awaitItem() + assertThat(messageSentState.text).isEqualTo(StableCharSequence("")) + assertThat(messageSentState.isSendButtonVisible).isFalse() + assertThat(fakeMatrixRoom.editMessageCalls.first()).isEqualTo(ANOTHER_MESSAGE) } } @@ -474,6 +507,10 @@ class MessageComposerPresenterTest { ) } -fun anEditMode() = MessageComposerMode.Edit(AN_EVENT_ID, A_MESSAGE) +fun anEditMode( + eventId: EventId? = AN_EVENT_ID, + message: String = A_MESSAGE, + transactionId: String? = null, +) = MessageComposerMode.Edit(eventId, message, transactionId) fun aReplyMode() = MessageComposerMode.Reply(A_USER_NAME, null, AN_EVENT_ID, A_MESSAGE) fun aQuoteMode() = MessageComposerMode.Quote(AN_EVENT_ID, A_MESSAGE) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index c1c6651cad..a655ffc25e 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -71,7 +71,7 @@ interface MatrixRoom : Closeable { suspend fun sendMessage(message: String): Result - suspend fun editMessage(originalEventId: EventId, message: String): Result + suspend fun editMessage(originalEventId: EventId?, transactionId: String?, message: String): Result suspend fun replyMessage(eventId: EventId, message: String): Result diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 361b6b1e18..1023b21225 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -34,6 +34,7 @@ import io.element.android.libraries.matrix.api.room.MessageEventType import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.api.timeline.MatrixTimeline +import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState import io.element.android.libraries.matrix.api.timeline.item.event.EventType import io.element.android.libraries.matrix.impl.core.toProgressWatcher import io.element.android.libraries.matrix.impl.room.location.toInner @@ -46,6 +47,7 @@ import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -209,11 +211,16 @@ class RustMatrixRoom( } } - override suspend fun editMessage(originalEventId: EventId, message: String): Result = withContext(coroutineDispatchers.io) { - val transactionId = genTransactionId() - // val content = messageEventContentFromMarkdown(message) - runCatching { - innerRoom.edit(/* TODO use content */ message, originalEventId.value, transactionId) + override suspend fun editMessage(originalEventId: EventId?, transactionId: String?, message: String): Result = withContext(coroutineDispatchers.io) { + if (originalEventId != null) { + runCatching { + innerRoom.edit(/* TODO use content */ message, originalEventId.value, transactionId) + } + } else { + runCatching { + transactionId?.let { cancelSend(it) } + innerRoom.send(messageEventContentFromMarkdown(message), genTransactionId()) + } } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt index 767a3ce444..866dbf984b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt @@ -110,4 +110,8 @@ class RustMatrixTimeline( innerRoom.sendReadReceipt(eventId = eventId.value) } } + + fun getItemById(eventId: EventId): MatrixTimelineItem.Event? { + return _timelineItems.value.firstOrNull { (it as? MatrixTimelineItem.Event)?.eventId == eventId } as? MatrixTimelineItem.Event + } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index e40c3b4331..f5b1cbddc3 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -81,6 +81,7 @@ class FakeMatrixRoom( private var reportContentResult = Result.success(Unit) private var sendLocationResult = Result.success(Unit) private var progressCallbackValues = emptyList>() + val editMessageCalls = mutableListOf() var sendMediaCount = 0 private set @@ -174,11 +175,8 @@ class FakeMatrixRoom( return cancelSendResult } - var editMessageParameter: String? = null - private set - - override suspend fun editMessage(originalEventId: EventId, message: String): Result { - editMessageParameter = message + override suspend fun editMessage(originalEventId: EventId?, transactionId: String?, message: String): Result { + editMessageCalls += message return Result.success(Unit) } diff --git a/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/MessageComposerMode.kt b/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/MessageComposerMode.kt index 5539b781ea..f0ccc76f3c 100644 --- a/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/MessageComposerMode.kt +++ b/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/MessageComposerMode.kt @@ -25,11 +25,11 @@ sealed interface MessageComposerMode : Parcelable { @Parcelize data class Normal(val content: CharSequence?) : MessageComposerMode - sealed class Special(open val eventId: EventId, open val defaultContent: CharSequence) : + sealed class Special(open val eventId: EventId?, open val defaultContent: CharSequence) : MessageComposerMode @Parcelize - data class Edit(override val eventId: EventId, override val defaultContent: CharSequence) : + data class Edit(override val eventId: EventId?, override val defaultContent: CharSequence, val transactionId: String?) : Special(eventId, defaultContent) @Parcelize diff --git a/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt b/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt index b13a361188..e9710da4d8 100644 --- a/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt +++ b/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt @@ -473,7 +473,7 @@ private fun EditContentToPreview() { TextComposer( onSendMessage = {}, onComposerTextChange = {}, - composerMode = MessageComposerMode.Edit(EventId("$1234"), "Some text"), + composerMode = MessageComposerMode.Edit(EventId("$1234"), "Some text", "1234"), onResetComposerMode = {}, composerCanSendMessage = true, composerText = "A message", diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentDarkPreview_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentDarkPreview_0_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..db0ce31cf4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentDarkPreview_0_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12f2f8146898375b4556dfd0718b02b18ac99859efe7e857914b2cf424ca17ba +size 25792 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentLightPreview_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentLightPreview_0_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..5b4c02582e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentLightPreview_0_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2822e203b85195a5584fa0d3e0be2b260ff233e725ae104f237c46978f6ae068 +size 27337 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.debug_null_DefaultGroup_EventDebugInfoViewPreviewDark_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.debug_null_DefaultGroup_EventDebugInfoViewPreviewDark_0_null,NEXUS_5,1.0,en].png index a903cb9526..ec77ebc1ac 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.debug_null_DefaultGroup_EventDebugInfoViewPreviewDark_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.debug_null_DefaultGroup_EventDebugInfoViewPreviewDark_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:15c77e539cc793ab0875813ac98eab14412a0619ca16afc90513e11f41462e07 -size 32575 +oid sha256:b28bdd7f227340b1af4456f0e24a78b8b279819b7e41edca11c0f2bc9a14a15b +size 32894 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.debug_null_DefaultGroup_EventDebugInfoViewPreviewLight_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.debug_null_DefaultGroup_EventDebugInfoViewPreviewLight_0_null,NEXUS_5,1.0,en].png index 91e74ddeb4..b86be1ed83 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.debug_null_DefaultGroup_EventDebugInfoViewPreviewLight_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.debug_null_DefaultGroup_EventDebugInfoViewPreviewLight_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:047b5e935803b2c7b12ebb1d86feabccac9d6291de3c14fea1185c2279db8d57 -size 35318 +oid sha256:f7d39ebcf85878e3869d369a10f95efe6c786db44c098864473b3e5bda51bd83 +size 35141