From e04780fbf6d7c50c93940f114303882024074451 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Fri, 5 Jan 2024 12:40:10 +0100 Subject: [PATCH] Trim whitespace at the end of messages (#2169) Trim whitespace at the end of messages --- changelog.d/2099.bugfix | 1 + .../TimelineItemContentMessageFactory.kt | 64 +++++++++++-------- .../matrix/ui/messages/ToHtmlDocument.kt | 6 +- 3 files changed, 42 insertions(+), 29 deletions(-) create mode 100644 changelog.d/2099.bugfix diff --git a/changelog.d/2099.bugfix b/changelog.d/2099.bugfix new file mode 100644 index 0000000000..f80120ae0d --- /dev/null +++ b/changelog.d/2099.bugfix @@ -0,0 +1 @@ +Trim whitespace at the end of messages to ensure we render the right content. 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 f02140847e..733b9b8878 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 @@ -72,7 +72,7 @@ class TimelineItemContentMessageFactory @Inject constructor( suspend fun create(content: MessageContent, senderDisplayName: String, eventId: EventId?): TimelineItemEventContent { return when (val messageType = content.type) { is EmoteMessageType -> { - val emoteBody = "* $senderDisplayName ${messageType.body}" + val emoteBody = "* $senderDisplayName ${messageType.body.trimEnd()}" TimelineItemEmoteContent( body = emoteBody, htmlDocument = messageType.formatted?.toHtmlDocument(prefix = "* $senderDisplayName"), @@ -83,7 +83,7 @@ class TimelineItemContentMessageFactory @Inject constructor( is ImageMessageType -> { val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height) TimelineItemImageContent( - body = messageType.body, + body = messageType.body.trimEnd(), mediaSource = messageType.source, thumbnailSource = messageType.info?.thumbnailSource, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, @@ -98,7 +98,7 @@ class TimelineItemContentMessageFactory @Inject constructor( is StickerMessageType -> { val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height) TimelineItemStickerContent( - body = messageType.body, + body = messageType.body.trimEnd(), mediaSource = messageType.source, thumbnailSource = messageType.info?.thumbnailSource, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, @@ -113,16 +113,17 @@ class TimelineItemContentMessageFactory @Inject constructor( is LocationMessageType -> { val location = Location.fromGeoUri(messageType.geoUri) if (location == null) { + val body = messageType.body.trimEnd() TimelineItemTextContent( - body = messageType.body, + body = body, htmlDocument = null, - plainText = messageType.body, + plainText = body, formattedBody = null, isEdited = content.isEdited, ) } else { TimelineItemLocationContent( - body = messageType.body, + body = messageType.body.trimEnd(), location = location, description = messageType.description ) @@ -131,7 +132,7 @@ class TimelineItemContentMessageFactory @Inject constructor( is VideoMessageType -> { val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height) TimelineItemVideoContent( - body = messageType.body, + body = messageType.body.trimEnd(), thumbnailSource = messageType.info?.thumbnailSource, videoSource = messageType.source, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, @@ -146,7 +147,7 @@ class TimelineItemContentMessageFactory @Inject constructor( } is AudioMessageType -> { TimelineItemAudioContent( - body = messageType.body, + body = messageType.body.trimEnd(), mediaSource = messageType.source, duration = messageType.info?.duration ?: Duration.ZERO, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, @@ -159,7 +160,7 @@ class TimelineItemContentMessageFactory @Inject constructor( true -> { TimelineItemVoiceContent( eventId = eventId, - body = messageType.body, + body = messageType.body.trimEnd(), mediaSource = messageType.source, duration = messageType.info?.duration ?: Duration.ZERO, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, @@ -168,7 +169,7 @@ class TimelineItemContentMessageFactory @Inject constructor( } false -> { TimelineItemAudioContent( - body = messageType.body, + body = messageType.body.trimEnd(), mediaSource = messageType.source, duration = messageType.info?.duration ?: Duration.ZERO, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, @@ -181,7 +182,7 @@ class TimelineItemContentMessageFactory @Inject constructor( is FileMessageType -> { val fileExtension = fileExtensionExtractor.extractFromName(messageType.body) TimelineItemFileContent( - body = messageType.body, + body = messageType.body.trimEnd(), thumbnailSource = messageType.info?.thumbnailSource, fileSource = messageType.source, mimeType = messageType.info?.mimetype ?: MimeTypes.fromFileExtension(fileExtension), @@ -189,26 +190,33 @@ class TimelineItemContentMessageFactory @Inject constructor( fileExtension = fileExtension ) } - is NoticeMessageType -> TimelineItemNoticeContent( - body = messageType.body, - htmlDocument = messageType.formatted?.toHtmlDocument(), - formattedBody = parseHtml(messageType.formatted) ?: messageType.body.withLinks(), - isEdited = content.isEdited, - ) - is TextMessageType -> { - TimelineItemTextContent( - body = messageType.body, + is NoticeMessageType -> { + val body = messageType.body.trimEnd() + TimelineItemNoticeContent( + body = body, htmlDocument = messageType.formatted?.toHtmlDocument(), - formattedBody = parseHtml(messageType.formatted) ?: messageType.body.withLinks(), + formattedBody = parseHtml(messageType.formatted) ?:body.withLinks(), + isEdited = content.isEdited, + ) + } + is TextMessageType -> { + val body = messageType.body.trimEnd() + TimelineItemTextContent( + body = body, + htmlDocument = messageType.formatted?.toHtmlDocument(), + formattedBody = parseHtml(messageType.formatted) ?: body.withLinks(), + isEdited = content.isEdited, + ) + } + is OtherMessageType -> { + val body = messageType.body.trimEnd() + TimelineItemTextContent( + body = body, + htmlDocument = null, + formattedBody = body.withLinks(), isEdited = content.isEdited, ) } - is OtherMessageType -> TimelineItemTextContent( - body = messageType.body, - htmlDocument = null, - formattedBody = messageType.body.withLinks(), - isEdited = content.isEdited, - ) } } @@ -225,7 +233,7 @@ class TimelineItemContentMessageFactory @Inject constructor( private fun parseHtml(formattedBody: FormattedBody?, prefix: String? = null): CharSequence? { if (formattedBody == null || formattedBody.format != MessageFormat.HTML) return null val result = htmlConverterProvider.provide() - .fromHtmlToSpans(formattedBody.body) + .fromHtmlToSpans(formattedBody.body.trimEnd()) .withFixedURLSpans() return if (prefix != null) { buildSpannedString { diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/ToHtmlDocument.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/ToHtmlDocument.kt index 8db1e6b5db..056aa5e8be 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/ToHtmlDocument.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/ToHtmlDocument.kt @@ -32,7 +32,11 @@ import org.jsoup.nodes.Document * @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 -> + return takeIf { it.format == MessageFormat.HTML }?.body + // Trim whitespace at the end to avoid having wrong rendering of the message. + // We don't trim the start in case it's used as indentation. + ?.trimEnd() + ?.let { formattedBody -> val dom = if (prefix != null) { Jsoup.parse("$prefix $formattedBody") } else {