Trim whitespace at the end of messages (#2169)

Trim whitespace at the end of messages
This commit is contained in:
Jorge Martin Espinosa 2024-01-05 12:40:10 +01:00 committed by GitHub
parent 936127c682
commit 0e3f0ac6b5
3 changed files with 42 additions and 29 deletions

1
changelog.d/2099.bugfix Normal file
View file

@ -0,0 +1 @@
Trim whitespace at the end of messages to ensure we render the right content.

View file

@ -72,7 +72,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
suspend fun create(content: MessageContent, senderDisplayName: String, eventId: EventId?): TimelineItemEventContent { suspend fun create(content: MessageContent, senderDisplayName: String, eventId: EventId?): TimelineItemEventContent {
return when (val messageType = content.type) { return when (val messageType = content.type) {
is EmoteMessageType -> { is EmoteMessageType -> {
val emoteBody = "* $senderDisplayName ${messageType.body}" val emoteBody = "* $senderDisplayName ${messageType.body.trimEnd()}"
TimelineItemEmoteContent( TimelineItemEmoteContent(
body = emoteBody, body = emoteBody,
htmlDocument = messageType.formatted?.toHtmlDocument(prefix = "* $senderDisplayName"), htmlDocument = messageType.formatted?.toHtmlDocument(prefix = "* $senderDisplayName"),
@ -83,7 +83,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
is ImageMessageType -> { is ImageMessageType -> {
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height) val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
TimelineItemImageContent( TimelineItemImageContent(
body = messageType.body, body = messageType.body.trimEnd(),
mediaSource = messageType.source, mediaSource = messageType.source,
thumbnailSource = messageType.info?.thumbnailSource, thumbnailSource = messageType.info?.thumbnailSource,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
@ -98,7 +98,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
is StickerMessageType -> { is StickerMessageType -> {
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height) val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
TimelineItemStickerContent( TimelineItemStickerContent(
body = messageType.body, body = messageType.body.trimEnd(),
mediaSource = messageType.source, mediaSource = messageType.source,
thumbnailSource = messageType.info?.thumbnailSource, thumbnailSource = messageType.info?.thumbnailSource,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
@ -113,16 +113,17 @@ class TimelineItemContentMessageFactory @Inject constructor(
is LocationMessageType -> { is LocationMessageType -> {
val location = Location.fromGeoUri(messageType.geoUri) val location = Location.fromGeoUri(messageType.geoUri)
if (location == null) { if (location == null) {
val body = messageType.body.trimEnd()
TimelineItemTextContent( TimelineItemTextContent(
body = messageType.body, body = body,
htmlDocument = null, htmlDocument = null,
plainText = messageType.body, plainText = body,
formattedBody = null, formattedBody = null,
isEdited = content.isEdited, isEdited = content.isEdited,
) )
} else { } else {
TimelineItemLocationContent( TimelineItemLocationContent(
body = messageType.body, body = messageType.body.trimEnd(),
location = location, location = location,
description = messageType.description description = messageType.description
) )
@ -131,7 +132,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
is VideoMessageType -> { is VideoMessageType -> {
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height) val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
TimelineItemVideoContent( TimelineItemVideoContent(
body = messageType.body, body = messageType.body.trimEnd(),
thumbnailSource = messageType.info?.thumbnailSource, thumbnailSource = messageType.info?.thumbnailSource,
videoSource = messageType.source, videoSource = messageType.source,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
@ -146,7 +147,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
} }
is AudioMessageType -> { is AudioMessageType -> {
TimelineItemAudioContent( TimelineItemAudioContent(
body = messageType.body, body = messageType.body.trimEnd(),
mediaSource = messageType.source, mediaSource = messageType.source,
duration = messageType.info?.duration ?: Duration.ZERO, duration = messageType.info?.duration ?: Duration.ZERO,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
@ -159,7 +160,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
true -> { true -> {
TimelineItemVoiceContent( TimelineItemVoiceContent(
eventId = eventId, eventId = eventId,
body = messageType.body, body = messageType.body.trimEnd(),
mediaSource = messageType.source, mediaSource = messageType.source,
duration = messageType.info?.duration ?: Duration.ZERO, duration = messageType.info?.duration ?: Duration.ZERO,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
@ -168,7 +169,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
} }
false -> { false -> {
TimelineItemAudioContent( TimelineItemAudioContent(
body = messageType.body, body = messageType.body.trimEnd(),
mediaSource = messageType.source, mediaSource = messageType.source,
duration = messageType.info?.duration ?: Duration.ZERO, duration = messageType.info?.duration ?: Duration.ZERO,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
@ -181,7 +182,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
is FileMessageType -> { is FileMessageType -> {
val fileExtension = fileExtensionExtractor.extractFromName(messageType.body) val fileExtension = fileExtensionExtractor.extractFromName(messageType.body)
TimelineItemFileContent( TimelineItemFileContent(
body = messageType.body, body = messageType.body.trimEnd(),
thumbnailSource = messageType.info?.thumbnailSource, thumbnailSource = messageType.info?.thumbnailSource,
fileSource = messageType.source, fileSource = messageType.source,
mimeType = messageType.info?.mimetype ?: MimeTypes.fromFileExtension(fileExtension), mimeType = messageType.info?.mimetype ?: MimeTypes.fromFileExtension(fileExtension),
@ -189,26 +190,33 @@ class TimelineItemContentMessageFactory @Inject constructor(
fileExtension = fileExtension fileExtension = fileExtension
) )
} }
is NoticeMessageType -> TimelineItemNoticeContent( is NoticeMessageType -> {
body = messageType.body, val body = messageType.body.trimEnd()
htmlDocument = messageType.formatted?.toHtmlDocument(), TimelineItemNoticeContent(
formattedBody = parseHtml(messageType.formatted) ?: messageType.body.withLinks(), body = body,
isEdited = content.isEdited,
)
is TextMessageType -> {
TimelineItemTextContent(
body = messageType.body,
htmlDocument = messageType.formatted?.toHtmlDocument(), 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, 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? { private fun parseHtml(formattedBody: FormattedBody?, prefix: String? = null): CharSequence? {
if (formattedBody == null || formattedBody.format != MessageFormat.HTML) return null if (formattedBody == null || formattedBody.format != MessageFormat.HTML) return null
val result = htmlConverterProvider.provide() val result = htmlConverterProvider.provide()
.fromHtmlToSpans(formattedBody.body) .fromHtmlToSpans(formattedBody.body.trimEnd())
.withFixedURLSpans() .withFixedURLSpans()
return if (prefix != null) { return if (prefix != null) {
buildSpannedString { buildSpannedString {

View file

@ -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. * @param prefix if not null, the prefix will be inserted at the beginning of the message.
*/ */
fun FormattedBody.toHtmlDocument(prefix: String? = null): Document? { 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) { val dom = if (prefix != null) {
Jsoup.parse("$prefix $formattedBody") Jsoup.parse("$prefix $formattedBody")
} else { } else {