From d413aa1ee3abd63a6f8a8ed27526e50e4f9f62ba Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Thu, 23 Nov 2023 08:29:20 +0100 Subject: [PATCH] Add plain text representation of messages (#1850) * Add plain text representation of messages. This is used in the room list as the last message in a room, in the message summary when a message is selected, in the 'replying to' block, in the 'replied to' block in a message in the timeline, and in notifications. --- changelog.d/1850.feature | 1 + .../impl/timeline/TimelineStateProvider.kt | 4 +- .../components/TimelineItemEventRow.kt | 40 +++--- .../TimelineItemContentMessageFactory.kt | 15 ++- .../event/TimelineItemEventFactory.kt | 3 +- .../impl/timeline/model/InReplyToDetails.kt | 52 ++++++++ .../impl/timeline/model/TimelineItem.kt | 3 +- .../model/event/TimelineItemEmoteContent.kt | 2 + .../model/event/TimelineItemNoticeContent.kt | 2 + .../event/TimelineItemTextBasedContent.kt | 1 + .../model/event/TimelineItemTextContent.kt | 2 + .../MessageSummaryFormatterImpl.kt | 2 +- .../messages/fixtures/aMessageEvent.kt | 4 +- .../timeline/model/InReplyToDetailTest.kt | 102 +++++++++++++++ .../eventformatter/impl/build.gradle.kts | 1 + .../impl/DefaultRoomLastMessageFormatter.kt | 3 +- .../api/notification/NotificationData.kt | 1 + .../impl/notification/NotificationMapper.kt | 1 + libraries/matrixui/build.gradle.kts | 5 + .../matrix/ui/messages/ToHtmlDocument.kt | 31 ++++- .../matrix/ui/messages/ToPlainText.kt | 82 ++++++++++++ .../matrixui/messages/ToHtmlDocumentTest.kt | 98 ++++++++++++++ .../matrixui/messages/ToPlainTextTest.kt | 122 ++++++++++++++++++ .../notifications/NotifiableEventResolver.kt | 6 +- 24 files changed, 544 insertions(+), 39 deletions(-) create mode 100644 changelog.d/1850.feature create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToDetails.kt create mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/model/InReplyToDetailTest.kt rename features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/util/toHtmlDocument.kt => libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/ToHtmlDocument.kt (52%) create mode 100644 libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/ToPlainText.kt create mode 100644 libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrixui/messages/ToHtmlDocumentTest.kt create mode 100644 libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrixui/messages/ToPlainTextTest.kt diff --git a/changelog.d/1850.feature b/changelog.d/1850.feature new file mode 100644 index 0000000000..275a6e954a --- /dev/null +++ b/changelog.d/1850.feature @@ -0,0 +1 @@ +Add plain text representation of messages 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 3defab97f5..1cc7daa498 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 @@ -16,6 +16,7 @@ package io.element.android.features.messages.impl.timeline +import io.element.android.features.messages.impl.timeline.model.InReplyToDetails import io.element.android.features.messages.impl.timeline.model.ReadReceiptData import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition @@ -34,7 +35,6 @@ import io.element.android.libraries.matrix.api.core.TransactionId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo -import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -122,7 +122,7 @@ internal fun aTimelineItemEvent( content: TimelineItemEventContent = aTimelineItemTextContent(), groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None, sendState: LocalEventSendState? = null, - inReplyTo: InReplyTo? = null, + inReplyTo: InReplyToDetails? = null, isThreaded: Boolean = false, debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(), timelineItemReactions: TimelineItemReactions = aTimelineItemReactions(), 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 03b1afdf13..850f709847 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 @@ -68,6 +68,7 @@ import io.element.android.features.messages.impl.timeline.components.event.Timel import io.element.android.features.messages.impl.timeline.components.event.toExtraPadding import io.element.android.features.messages.impl.timeline.components.receipt.ReadReceiptViewState import io.element.android.features.messages.impl.timeline.components.receipt.TimelineItemReadReceiptView +import io.element.android.features.messages.impl.timeline.model.InReplyToDetails import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition import io.element.android.features.messages.impl.timeline.model.bubble.BubbleState @@ -97,7 +98,6 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType -import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent import io.element.android.libraries.matrix.api.timeline.item.event.PollContent @@ -142,7 +142,7 @@ fun TimelineItemEventRow( } fun inReplyToClicked() { - val inReplyToEventId = (event.inReplyTo as? InReplyTo.Ready)?.eventId ?: return + val inReplyToEventId = event.inReplyTo?.eventId ?: return inReplyToClick(inReplyToEventId) } @@ -497,7 +497,7 @@ private fun MessageEventBubbleContent( fun CommonLayout( timestampPosition: TimestampPosition, showThreadDecoration: Boolean, - inReplyToDetails: InReplyTo.Ready?, + inReplyToDetails: InReplyToDetails?, modifier: Modifier = Modifier ) { val timestampLayoutModifier: Modifier @@ -543,10 +543,10 @@ private fun MessageEventBubbleContent( ) } } - val inReplyTo = @Composable { inReplyToReady: InReplyTo.Ready -> - val senderName = inReplyToReady.senderDisplayName ?: inReplyToReady.senderId.value - val attachmentThumbnailInfo = attachmentThumbnailInfoForInReplyTo(inReplyToReady) - val text = textForInReplyTo(inReplyToReady) + val inReplyTo = @Composable { inReplyTo: InReplyToDetails -> + val senderName = inReplyTo.senderDisplayName ?: inReplyTo.senderId.value + val attachmentThumbnailInfo = attachmentThumbnailInfoForInReplyTo(inReplyTo) + val text = textForInReplyTo(inReplyTo) val topPadding = if (showThreadDecoration) 0.dp else 8.dp ReplyToContent( senderName = senderName, @@ -581,11 +581,10 @@ private fun MessageEventBubbleContent( is TimelineItemPollContent -> TimestampPosition.Below else -> TimestampPosition.Default } - val replyToDetails = event.inReplyTo as? InReplyTo.Ready CommonLayout( showThreadDecoration = event.isThreaded, timestampPosition = timestampPosition, - inReplyToDetails = replyToDetails, + inReplyToDetails = event.inReplyTo, modifier = bubbleModifier ) } @@ -638,8 +637,8 @@ private fun ReplyToContent( } } -private fun attachmentThumbnailInfoForInReplyTo(inReplyTo: InReplyTo.Ready): AttachmentThumbnailInfo? { - return when (val eventContent = inReplyTo.content) { +private fun attachmentThumbnailInfoForInReplyTo(inReplyTo: InReplyToDetails): AttachmentThumbnailInfo? { + return when (val eventContent = inReplyTo.eventContent) { is MessageContent -> when (val type = eventContent.type) { is ImageMessageType -> AttachmentThumbnailInfo( thumbnailSource = type.info?.thumbnailSource ?: type.source, @@ -680,12 +679,12 @@ private fun attachmentThumbnailInfoForInReplyTo(inReplyTo: InReplyTo.Ready): Att } @Composable -private fun textForInReplyTo(inReplyTo: InReplyTo.Ready): String { - return when (val eventContent = inReplyTo.content) { +private fun textForInReplyTo(inReplyTo: InReplyToDetails): String { + return when (val eventContent = inReplyTo.eventContent) { is MessageContent -> when (eventContent.type) { is LocationMessageType -> stringResource(CommonStrings.common_shared_location) is VoiceMessageType -> stringResource(CommonStrings.common_voice_message) - else -> eventContent.body + else -> inReplyTo.textContent ?: eventContent.body } is PollContent -> eventContent.question else -> "" @@ -769,7 +768,7 @@ internal fun TimelineItemEventRowWithReplyPreview() = ElementPreview { body = "A long text which will be displayed on several lines and" + " hopefully can be manually adjusted to test different behaviors." ), - inReplyTo = aInReplyToReady(replyContent), + inReplyTo = aInReplyToDetails(replyContent), groupPosition = TimelineItemGroupPosition.First, ), showReadReceipts = false, @@ -794,7 +793,7 @@ internal fun TimelineItemEventRowWithReplyPreview() = ElementPreview { content = aTimelineItemImageContent().copy( aspectRatio = 5f ), - inReplyTo = aInReplyToReady(replyContent), + inReplyTo = aInReplyToDetails(replyContent), isThreaded = true, groupPosition = TimelineItemGroupPosition.Last, ), @@ -818,15 +817,16 @@ internal fun TimelineItemEventRowWithReplyPreview() = ElementPreview { } } -private fun aInReplyToReady( +private fun aInReplyToDetails( replyContent: String, -): InReplyTo.Ready { - return InReplyTo.Ready( +): InReplyToDetails { + return InReplyToDetails( eventId = EventId("\$event"), - content = MessageContent(replyContent, null, false, false, TextMessageType(replyContent, null)), + eventContent = MessageContent(replyContent, null, false, false, TextMessageType(replyContent, null)), senderId = UserId("@Sender:domain"), senderDisplayName = "Sender", senderAvatarUrl = null, + textContent = replyContent, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt index bf4ba27bd2..58d4a23cf2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt @@ -28,7 +28,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent import io.element.android.features.messages.impl.timeline.util.FileExtensionExtractor -import io.element.android.features.messages.impl.timeline.util.toHtmlDocument import io.element.android.libraries.androidutils.filesize.FileSizeFormatter import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.featureflag.api.FeatureFlagService @@ -46,6 +45,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageTy import io.element.android.libraries.matrix.api.timeline.item.event.UnknownMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType +import io.element.android.libraries.matrix.ui.messages.toHtmlDocument import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import java.time.Duration @@ -85,6 +85,7 @@ class TimelineItemContentMessageFactory @Inject constructor( TimelineItemTextContent( body = messageType.body, htmlDocument = null, + plainText = messageType.body, isEdited = content.isEdited, ) } else { @@ -161,11 +162,13 @@ class TimelineItemContentMessageFactory @Inject constructor( htmlDocument = messageType.formatted?.toHtmlDocument(), isEdited = content.isEdited, ) - is TextMessageType -> TimelineItemTextContent( - body = messageType.body, - htmlDocument = messageType.formatted?.toHtmlDocument(), - isEdited = content.isEdited, - ) + is TextMessageType -> { + TimelineItemTextContent( + body = messageType.body, + htmlDocument = messageType.formatted?.toHtmlDocument(), + isEdited = content.isEdited, + ) + } is OtherMessageType -> TimelineItemTextContent( body = messageType.body, htmlDocument = null, 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 4c9217a884..837e84abc4 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 @@ -24,6 +24,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions import io.element.android.features.messages.impl.timeline.model.TimelineItemReadReceipts +import io.element.android.features.messages.impl.timeline.model.map import io.element.android.libraries.core.bool.orTrue import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter import io.element.android.libraries.designsystem.components.avatar.AvatarData @@ -92,7 +93,7 @@ class TimelineItemEventFactory @Inject constructor( reactionsState = currentTimelineItem.computeReactionsState(), readReceiptState = currentTimelineItem.computeReadReceiptState(roomMembers), localSendState = currentTimelineItem.event.localSendState, - inReplyTo = currentTimelineItem.event.inReplyTo(), + inReplyTo = currentTimelineItem.event.inReplyTo()?.map(), isThreaded = currentTimelineItem.event.isThreaded(), debugInfo = currentTimelineItem.event.debugInfo, origin = currentTimelineItem.event.origin, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToDetails.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToDetails.kt new file mode 100644 index 0000000000..14596ac1f0 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToDetails.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.timeline.model + +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.timeline.item.event.EventContent +import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo +import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent +import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType +import io.element.android.libraries.matrix.ui.messages.toPlainText + +data class InReplyToDetails( + val eventId: EventId, + val senderId: UserId, + val senderDisplayName: String?, + val senderAvatarUrl: String?, + val eventContent: EventContent?, + val textContent: String?, +) + +fun InReplyTo.map() = when (this) { + is InReplyTo.Ready -> InReplyToDetails( + eventId = eventId, + senderId = senderId, + senderDisplayName = senderDisplayName, + senderAvatarUrl = senderAvatarUrl, + eventContent = content, + textContent = when (content) { + is MessageContent -> { + val messageContent = content as MessageContent + (messageContent.type as? TextMessageType)?.toPlainText() ?: messageContent.body + } + else -> null + } + ) + else -> null +} 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 5ceaba5550..db0d88b462 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 @@ -25,7 +25,6 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.TransactionId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo -import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin import kotlinx.collections.immutable.ImmutableList @@ -67,7 +66,7 @@ sealed interface TimelineItem { val reactionsState: TimelineItemReactions, val readReceiptState: TimelineItemReadReceipts, val localSendState: LocalEventSendState?, - val inReplyTo: InReplyTo?, + val inReplyTo: InReplyToDetails?, val isThreaded: Boolean, val debugInfo: TimelineItemDebugInfo, val origin: TimelineItemEventOrigin?, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEmoteContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEmoteContent.kt index 27aa26ad80..d6ac2a7d95 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEmoteContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEmoteContent.kt @@ -16,11 +16,13 @@ package io.element.android.features.messages.impl.timeline.model.event +import io.element.android.libraries.matrix.ui.messages.toPlainText import org.jsoup.nodes.Document data class TimelineItemEmoteContent( override val body: String, override val htmlDocument: Document?, + override val plainText: String = htmlDocument?.toPlainText() ?: body, override val isEdited: Boolean, ) : TimelineItemTextBasedContent { override val type: String = "TimelineItemEmoteContent" diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemNoticeContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemNoticeContent.kt index 175268093e..fa0ec630cd 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemNoticeContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemNoticeContent.kt @@ -16,11 +16,13 @@ package io.element.android.features.messages.impl.timeline.model.event +import io.element.android.libraries.matrix.ui.messages.toPlainText import org.jsoup.nodes.Document data class TimelineItemNoticeContent( override val body: String, override val htmlDocument: Document?, + override val plainText: String = htmlDocument?.toPlainText() ?: body, override val isEdited: Boolean, ) : TimelineItemTextBasedContent { override val type: String = "TimelineItemNoticeContent" diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemTextBasedContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemTextBasedContent.kt index 5d5f121b75..edfc12eb95 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemTextBasedContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemTextBasedContent.kt @@ -23,6 +23,7 @@ import org.jsoup.nodes.Document sealed interface TimelineItemTextBasedContent : TimelineItemEventContent { val body: String val htmlDocument: Document? + val plainText: String val isEdited: Boolean val htmlBody: String? get() = htmlDocument?.body()?.html() diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemTextContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemTextContent.kt index ecb50af31c..a2518fbd7a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemTextContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemTextContent.kt @@ -16,11 +16,13 @@ package io.element.android.features.messages.impl.timeline.model.event +import io.element.android.libraries.matrix.ui.messages.toPlainText import org.jsoup.nodes.Document data class TimelineItemTextContent( override val body: String, override val htmlDocument: Document?, + override val plainText: String = htmlDocument?.toPlainText() ?: body, override val isEdited: Boolean, ) : TimelineItemTextBasedContent{ override val type: String = "TimelineItemTextContent" diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/messagesummary/MessageSummaryFormatterImpl.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/messagesummary/MessageSummaryFormatterImpl.kt index 6b3ed3d65e..762e43cf92 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/messagesummary/MessageSummaryFormatterImpl.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/messagesummary/MessageSummaryFormatterImpl.kt @@ -43,7 +43,7 @@ class MessageSummaryFormatterImpl @Inject constructor( ) : MessageSummaryFormatter { override fun format(event: TimelineItem.Event): String { return when (event.content) { - is TimelineItemTextBasedContent -> event.content.body + is TimelineItemTextBasedContent -> event.content.plainText is TimelineItemProfileChangeContent -> event.content.body is TimelineItemStateContent -> event.content.body is TimelineItemLocationContent -> context.getString(CommonStrings.common_shared_location) 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 68c80d6b80..7d87830605 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 @@ -17,6 +17,7 @@ package io.element.android.features.messages.fixtures import io.element.android.features.messages.impl.timeline.aTimelineItemReactions +import io.element.android.features.messages.impl.timeline.model.InReplyToDetails import io.element.android.features.messages.impl.timeline.model.ReadReceiptData import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItemReadReceipts @@ -27,7 +28,6 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState -import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo 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_USER_ID @@ -39,7 +39,7 @@ internal fun aMessageEvent( eventId: EventId? = AN_EVENT_ID, isMine: Boolean = true, content: TimelineItemEventContent = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false), - inReplyTo: InReplyTo? = null, + inReplyTo: InReplyToDetails? = null, isThreaded: Boolean = false, debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(), sendState: LocalEventSendState = LocalEventSendState.Sent(AN_EVENT_ID), diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/model/InReplyToDetailTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/model/InReplyToDetailTest.kt new file mode 100644 index 0000000000..20b18a066b --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/model/InReplyToDetailTest.kt @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.timeline.model + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.messages.impl.timeline.model.map +import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody +import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo +import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange +import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent +import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat +import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent +import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.A_USER_ID +import org.junit.Test + +class InReplyToDetailTest { + + @Test + fun `map - with a not ready InReplyTo does not work`() { + assertThat(InReplyTo.Pending.map()).isNull() + assertThat(InReplyTo.NotLoaded(AN_EVENT_ID).map()).isNull() + assertThat(InReplyTo.Error.map()).isNull() + } + + @Test + fun `map - with something other than a MessageContent has no textContent`() { + val inReplyTo = InReplyTo.Ready( + eventId = AN_EVENT_ID, + senderId = A_USER_ID, + senderDisplayName = "senderDisplayName", + senderAvatarUrl = "senderAvatarUrl", + content = RoomMembershipContent( + userId = A_USER_ID, + change = MembershipChange.INVITED, + ) + ) + val inReplyToDetails = inReplyTo.map() + assertThat(inReplyToDetails).isNotNull() + assertThat(inReplyToDetails?.textContent).isNull() + } + + @Test + fun `map - with a message content tries to use the formatted text if exists for its textContent`() { + val inReplyTo = InReplyTo.Ready( + eventId = AN_EVENT_ID, + senderId = A_USER_ID, + senderDisplayName = "senderDisplayName", + senderAvatarUrl = "senderAvatarUrl", + content = MessageContent( + body = "**Hello!**", + inReplyTo = null, + isEdited = false, + isThreaded = false, + type = TextMessageType( + body = "**Hello!**", + formatted = FormattedBody( + format = MessageFormat.HTML, + body = "

Hello!

" + ) + ) + ) + ) + assertThat(inReplyTo.map()?.textContent).isEqualTo("Hello!") + } + + @Test + fun `map - with a message content and no formatted body uses body as fallback for textContent`() { + val inReplyTo = InReplyTo.Ready( + eventId = AN_EVENT_ID, + senderId = A_USER_ID, + senderDisplayName = "senderDisplayName", + senderAvatarUrl = "senderAvatarUrl", + content = MessageContent( + body = "**Hello!**", + inReplyTo = null, + isEdited = false, + isThreaded = false, + type = TextMessageType( + body = "**Hello!**", + formatted = null, + ) + ) + ) + assertThat(inReplyTo.map()?.textContent).isEqualTo("**Hello!**") + } +} diff --git a/libraries/eventformatter/impl/build.gradle.kts b/libraries/eventformatter/impl/build.gradle.kts index 3bee2df488..1fa92f1e25 100644 --- a/libraries/eventformatter/impl/build.gradle.kts +++ b/libraries/eventformatter/impl/build.gradle.kts @@ -40,6 +40,7 @@ dependencies { implementation(projects.libraries.core) implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrixui) implementation(projects.libraries.uiStrings) implementation(projects.services.toolbox.api) api(projects.libraries.eventformatter.api) diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt index aad89c5c2c..d60802b471 100644 --- a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt @@ -51,6 +51,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.UnknownConten import io.element.android.libraries.matrix.api.timeline.item.event.UnknownMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType +import io.element.android.libraries.matrix.ui.messages.toPlainText import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.services.toolbox.api.strings.StringProvider import javax.inject.Inject @@ -114,7 +115,7 @@ class DefaultRoomLastMessageFormatter @Inject constructor( return "* $senderDisplayName ${messageType.body}" } is TextMessageType -> { - messageType.body + messageType.toPlainText() } is VideoMessageType -> { sp.getString(CommonStrings.common_video) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt index 1abbf80130..08f40c3376 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt @@ -36,6 +36,7 @@ data class NotificationData( val content: NotificationContent, // For images for instance val contentUrl: String?, + val hasMention: Boolean, ) sealed interface NotificationContent { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt index 0d9f794173..9914a66c85 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt @@ -53,6 +53,7 @@ class NotificationMapper( timestamp = item.timestamp() ?: clock.epochMillis(), content = item.event.use { notificationContentMapper.map(it) }, contentUrl = null, + hasMention = item.hasMention.orFalse(), ) } } diff --git a/libraries/matrixui/build.gradle.kts b/libraries/matrixui/build.gradle.kts index 7302eeddfb..669f9c9634 100644 --- a/libraries/matrixui/build.gradle.kts +++ b/libraries/matrixui/build.gradle.kts @@ -40,6 +40,11 @@ dependencies { implementation(projects.libraries.uiStrings) implementation(libs.coil.compose) implementation(libs.coil.gif) + implementation(libs.jsoup) ksp(libs.showkase.processor) + + testImplementation(libs.test.junit) + testImplementation(libs.test.truth) + testImplementation(libs.test.robolectric) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/util/toHtmlDocument.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/ToHtmlDocument.kt similarity index 52% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/util/toHtmlDocument.kt rename to libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/ToHtmlDocument.kt index a38b631eb2..8db1e6b5db 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/util/toHtmlDocument.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/ToHtmlDocument.kt @@ -14,19 +14,46 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.timeline.util +package io.element.android.libraries.matrix.ui.messages +import io.element.android.libraries.matrix.api.permalink.PermalinkData +import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat import org.jsoup.Jsoup import org.jsoup.nodes.Document +/** + * Converts the HTML string [FormattedBody.body] to a [Document] by parsing it. + * If the message is not formatted or the format is not [MessageFormat.HTML] we return `null`. + * + * This will also make sure mentions are prefixed with `@`. + * + * @param prefix if not null, the prefix will be inserted at the beginning of the message. + */ fun FormattedBody.toHtmlDocument(prefix: String? = null): Document? { return takeIf { it.format == MessageFormat.HTML }?.body?.let { formattedBody -> - if (prefix != null) { + val dom = if (prefix != null) { Jsoup.parse("$prefix $formattedBody") } else { Jsoup.parse(formattedBody) } + + // Prepend `@` to mentions + fixMentions(dom) + + dom + } +} + +private fun fixMentions(dom: Document) { + val links = dom.getElementsByTag("a") + links.forEach { + if (it.hasAttr("href")) { + val link = PermalinkParser.parse(it.attr("href")) + if (link is PermalinkData.UserLink && !it.text().startsWith("@")) { + it.prependText("@") + } + } } } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/ToPlainText.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/ToPlainText.kt new file mode 100644 index 0000000000..f20252c42a --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/ToPlainText.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.libraries.matrix.ui.messages + +import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody +import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import org.jsoup.nodes.Node +import org.jsoup.nodes.TextNode +import org.jsoup.select.NodeVisitor + +/** + * Converts the HTML string in [TextMessageType.formatted] to a plain text representation by parsing it and removing all formatting. + * If the message is not formatted or the format is not [MessageFormat.HTML], the [TextMessageType.body] is returned instead. + */ +fun TextMessageType.toPlainText() = formatted?.toPlainText() ?: body + +/** + * Converts the HTML string in [FormattedBody.body] to a plain text representation by parsing it and removing all formatting. + * If the message is not formatted or the format is not [MessageFormat.HTML] we return `null`. + * @param prefix if not null, the prefix will be inserted at the beginning of the message. + */ +fun FormattedBody.toPlainText(prefix: String? = null): String? { + return this.toHtmlDocument(prefix)?.toPlainText() +} + +/** + * Converts the HTML [Document] to a plain text representation by parsing it and removing all formatting. + */ +fun Document.toPlainText(): String { + val visitor = PlainTextNodeVisitor() + traverse(visitor) + return visitor.build() +} + +private class PlainTextNodeVisitor : NodeVisitor { + private val builder = StringBuilder() + + override fun head(node: Node, depth: Int) { + if (node is TextNode && node.text().isNotBlank()) { + builder.append(node.text()) + } else if (node is Element && node.tagName() == "li") { + val index = node.elementSiblingIndex() + val isOrdered = node.parent()?.nodeName()?.lowercase() == "ol" + if (isOrdered) { + builder.append("${index + 1}. ") + } else { + builder.append("• ") + } + } else if (node is Element && node.isBlock && builder.lastOrNull() != '\n') { + builder.append("\n") + } + } + + override fun tail(node: Node, depth: Int) { + fun nodeIsBlockButNotLastOne(node: Node) = node is Element && node.isBlock && node.lastElementSibling() !== node + fun nodeIsLineBreak(node: Node) = node.nodeName().lowercase() == "br" + if (nodeIsBlockButNotLastOne(node) || nodeIsLineBreak(node)) { + builder.append("\n") + } + } + + fun build(): String { + return builder.toString().trim() + } +} diff --git a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrixui/messages/ToHtmlDocumentTest.kt b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrixui/messages/ToHtmlDocumentTest.kt new file mode 100644 index 0000000000..d6c740b77d --- /dev/null +++ b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrixui/messages/ToHtmlDocumentTest.kt @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.libraries.matrixui.messages + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody +import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat +import io.element.android.libraries.matrix.ui.messages.toHtmlDocument +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class ToHtmlDocumentTest { + + @Test + fun `toHtmlDocument - returns null if format is not HTML`() { + val body = FormattedBody( + format = MessageFormat.UNKNOWN, + body = "Hello world" + ) + + val document = body.toHtmlDocument() + + assertThat(document).isNull() + } + + @Test + fun `toHtmlDocument - returns a Document if the format is HTML`() { + val body = FormattedBody( + format = MessageFormat.HTML, + body = "

Hello world

" + ) + + val document = body.toHtmlDocument() + assertThat(document).isNotNull() + assertThat(document?.text()).isEqualTo("Hello world") + } + + @Test + fun `toHtmlDocument - returns a Document with a prefix if provided`() { + val body = FormattedBody( + format = MessageFormat.HTML, + body = "

Hello world

" + ) + + val document = body.toHtmlDocument(prefix = "@Jorge:") + assertThat(document).isNotNull() + assertThat(document?.text()).isEqualTo("@Jorge: Hello world") + } + + @Test + fun `toHtmlDocument - if a mention is found without an '@' prefix, it will be added`() { + val body = FormattedBody( + format = MessageFormat.HTML, + body = "Hey Alice!" + ) + + val document = body.toHtmlDocument() + assertThat(document?.text()).isEqualTo("Hey @Alice!") + } + + @Test + fun `toHtmlDocument - if a mention is found with an '@' prefix, nothing will be done`() { + val body = FormattedBody( + format = MessageFormat.HTML, + body = "Hey @Alice!" + ) + + val document = body.toHtmlDocument() + assertThat(document?.text()).isEqualTo("Hey @Alice!") + } + + @Test + fun `toHtmlDocument - if a link is not a mention, nothing will be done for it`() { + val body = FormattedBody( + format = MessageFormat.HTML, + body = "Hey Alice!" + ) + + val document = body.toHtmlDocument() + assertThat(document?.text()).isEqualTo("Hey Alice!") + } +} diff --git a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrixui/messages/ToPlainTextTest.kt b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrixui/messages/ToPlainTextTest.kt new file mode 100644 index 0000000000..3c1a469f42 --- /dev/null +++ b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrixui/messages/ToPlainTextTest.kt @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.libraries.matrixui.messages + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody +import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat +import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType +import io.element.android.libraries.matrix.ui.messages.toPlainText +import org.jsoup.Jsoup +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class ToPlainTextTest { + + @Test + fun `Document toPlainText - returns a plain text version of the document`() { + val document = Jsoup.parse( + """ + Hello world + +
  1. This is an ordered list.
+
+ """.trimIndent() + ) + + assertThat(document.toPlainText()).isEqualTo(""" + Hello world + • This is an unordered list. + 1. This is an ordered list. + """.trimIndent() + ) + } + + @Test + fun `FormattedBody toPlainText - returns a plain text version of the HTML body`() { + val formattedBody = FormattedBody( + format = MessageFormat.HTML, + body = """ + Hello world + +
  1. This is an ordered list.
+
+ """.trimIndent() + ) + assertThat(formattedBody.toPlainText()).isEqualTo(""" + Hello world + • This is an unordered list. + 1. This is an ordered list. + """.trimIndent() + ) + } + + @Test + fun `FormattedBody toPlainText - returns null if the format is not HTML`() { + val formattedBody = FormattedBody( + format = MessageFormat.UNKNOWN, + body = """ + Hello world + +
  1. This is an ordered list.
+
+ """.trimIndent() + ) + assertThat(formattedBody.toPlainText()).isNull() + } + + @Test + fun `TextMessageType toPlainText - returns a plain text version of the HTML body`() { + val messageType = TextMessageType( + body = "Hello world\n- This in an unordered list.\n1. This is an ordered list.\n", + formatted = FormattedBody( + format = MessageFormat.HTML, + body = """ + Hello world + +
  1. This is an ordered list.
+
+ """.trimIndent() + ) + ) + assertThat(messageType.toPlainText()).isEqualTo(""" + Hello world + • This is an unordered list. + 1. This is an ordered list. + """.trimIndent() + ) + } + + @Test + fun `TextMessageType toPlainText - returns the markdown body if the formatted one cannot be parsed`() { + val messageType = TextMessageType( + body = "This is the fallback text", + formatted = FormattedBody( + format = MessageFormat.UNKNOWN, + body = """ + Hello world + +
  1. This is an ordered list.
+
+ """.trimIndent() + ) + ) + assertThat(messageType.toPlainText()).isEqualTo("This is the fallback text") + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt index 82b076aa73..2710e119fd 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt @@ -37,6 +37,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageTy import io.element.android.libraries.matrix.api.timeline.item.event.UnknownMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType +import io.element.android.libraries.matrix.ui.messages.toPlainText import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent @@ -81,6 +82,7 @@ class NotifiableEventResolver @Inject constructor( private fun NotificationData.asNotifiableEvent(userId: SessionId): NotifiableEvent? { return when (val content = this.content) { is NotificationContent.MessageLike.RoomMessage -> { + val messageBody = descriptionFromMessageContent(content, senderDisplayName ?: content.senderId.value) buildNotifiableMessageEvent( sessionId = userId, senderId = content.senderId, @@ -89,7 +91,7 @@ class NotifiableEventResolver @Inject constructor( noisy = isNoisy, timestamp = this.timestamp, senderName = senderDisplayName, - body = descriptionFromMessageContent(content, senderDisplayName ?: content.senderId.value), + body = messageBody, imageUriString = this.contentUrl, roomName = roomDisplayName, roomIsDirect = isDirect, @@ -216,7 +218,7 @@ class NotifiableEventResolver @Inject constructor( is FileMessageType -> messageType.body is ImageMessageType -> messageType.body is NoticeMessageType -> messageType.body - is TextMessageType -> messageType.body + is TextMessageType -> messageType.toPlainText() is VideoMessageType -> messageType.body is LocationMessageType -> messageType.body is OtherMessageType -> messageType.body