diff --git a/CHANGES.md b/CHANGES.md index bb02b798e0..365ec0c8fc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,10 +8,13 @@ Features ✨: Improvements 🙌: - Better connectivity lost indicator when airplane mode is on - Add a setting to hide redacted events (#951) + - Render formatted_body for m.notice and m.emote (#1196) Bugfix 🐛: - After jump to unread, newer messages are never loaded (#1008) - Fix issues with FontScale switch (#69, #645) + - "Seen by" uses 12h time (#1378) + - Enable markdown (if active) when sending emote (#734) Translations 🗣: - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageContentWithFormattedBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageContentWithFormattedBody.kt new file mode 100644 index 0000000000..b51e3eb841 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageContentWithFormattedBody.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 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 im.vector.matrix.android.api.session.room.model.message + +interface MessageContentWithFormattedBody : MessageContent { + /** + * The format used in the formatted_body. Currently only "org.matrix.custom.html" is supported. + */ + val format: String? + + /** + * The formatted version of the body. This is required if format is specified. + */ + val formattedBody: String? + + /** + * Get the formattedBody, only if not blank and if the format is equal to "org.matrix.custom.html" + */ + val matrixFormattedBody: String? + get() = formattedBody?.takeIf { it.isNotBlank() && format == MessageFormat.FORMAT_MATRIX_HTML } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEmoteContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEmoteContent.kt index e7106a9755..7b63959f78 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEmoteContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEmoteContent.kt @@ -34,15 +34,15 @@ data class MessageEmoteContent( @Json(name = "body") override val body: String, /** - * The format used in the formatted_body. Currently only org.matrix.custom.html is supported. + * The format used in the formatted_body. Currently only "org.matrix.custom.html" is supported. */ - @Json(name = "format") val format: String? = null, + @Json(name = "format") override val format: String? = null, /** * The formatted version of the body. This is required if format is specified. */ - @Json(name = "formatted_body") val formattedBody: String? = null, + @Json(name = "formatted_body") override val formattedBody: String? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.new_content") override val newContent: Content? = null -) : MessageContent +) : MessageContentWithFormattedBody diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageNoticeContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageNoticeContent.kt index e08e07e9da..41e63bb457 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageNoticeContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageNoticeContent.kt @@ -34,15 +34,15 @@ data class MessageNoticeContent( @Json(name = "body") override val body: String, /** - * The format used in the formatted_body. Currently only org.matrix.custom.html is supported. + * The format used in the formatted_body. Currently only "org.matrix.custom.html" is supported. */ - @Json(name = "format") val format: String? = null, + @Json(name = "format") override val format: String? = null, /** * The formatted version of the body. This is required if format is specified. */ - @Json(name = "formatted_body") val formattedBody: String? = null, + @Json(name = "formatted_body") override val formattedBody: String? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.new_content") override val newContent: Content? = null -) : MessageContent +) : MessageContentWithFormattedBody diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageTextContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageTextContent.kt index cc5bb1f774..d6c54e3ff5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageTextContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageTextContent.kt @@ -34,15 +34,15 @@ data class MessageTextContent( @Json(name = "body") override val body: String, /** - * The format used in the formatted_body. Currently only org.matrix.custom.html is supported. + * The format used in the formatted_body. Currently only "org.matrix.custom.html" is supported. */ - @Json(name = "format") val format: String? = null, + @Json(name = "format") override val format: String? = null, /** * The formatted version of the body. This is required if format is specified. */ - @Json(name = "formatted_body") val formattedBody: String? = null, + @Json(name = "formatted_body") override val formattedBody: String? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.new_content") override val newContent: Content? = null -) : MessageContent +) : MessageContentWithFormattedBody diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/extensions/Strings.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/extensions/Strings.kt new file mode 100644 index 0000000000..09913b9f04 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/extensions/Strings.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2020 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 im.vector.matrix.android.internal.extensions + +/** + * Ex: "abcdef".subStringBetween("a", "f") -> "bcde" + * Ex: "abcdefff".subStringBetween("a", "f") -> "bcdeff" + * Ex: "aaabcdef".subStringBetween("a", "f") -> "aabcde" + */ +internal fun String.subStringBetween(prefix: String, suffix: String) = substringAfter(prefix).substringBeforeLast(suffix) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index 5f0515e669..2a24094b5d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -35,6 +35,7 @@ import im.vector.matrix.android.api.session.room.model.message.FileInfo import im.vector.matrix.android.api.session.room.model.message.ImageInfo import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageContentWithFormattedBody import im.vector.matrix.android.api.session.room.model.message.MessageFileContent import im.vector.matrix.android.api.session.room.model.message.MessageFormat import im.vector.matrix.android.api.session.room.model.message.MessageImageContent @@ -56,6 +57,7 @@ import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.internal.di.UserId +import im.vector.matrix.android.internal.extensions.subStringBetween import im.vector.matrix.android.internal.session.content.ThumbnailExtractor import im.vector.matrix.android.internal.session.room.send.pills.TextPillsUtils import im.vector.matrix.android.internal.task.TaskExecutor @@ -84,6 +86,7 @@ internal class LocalEchoEventFactory @Inject constructor( ) { // TODO Inject private val parser = Parser.builder().build() + // TODO Inject private val renderer = HtmlRenderer.builder().build() @@ -102,8 +105,15 @@ internal class LocalEchoEventFactory @Inject constructor( val document = parser.parse(source) val htmlText = renderer.render(document) - if (isFormattedTextPertinent(source, htmlText)) { - return TextContent(text.toString(), htmlText) + // Cleanup extra paragraph + val cleanHtmlText = if (htmlText.startsWith("
") && htmlText.endsWith("
\n")) { + htmlText.subStringBetween("", "
\n") + } else { + htmlText + } + + if (isFormattedTextPertinent(source, cleanHtmlText)) { + return TextContent(text.toString(), cleanHtmlText) } } else { // Try to detect pills @@ -433,10 +443,8 @@ internal class LocalEchoEventFactory @Inject constructor( MessageType.MSGTYPE_TEXT, MessageType.MSGTYPE_NOTICE -> { var formattedText: String? = null - if (content is MessageTextContent) { - if (content.format == MessageFormat.FORMAT_MATRIX_HTML) { - formattedText = content.formattedBody - } + if (content is MessageContentWithFormattedBody) { + formattedText = content.matrixFormattedBody } val isReply = content.isReply() || originalContent.isReply() return if (isReply) { diff --git a/vector/src/main/java/im/vector/riotx/core/date/VectorDateFormatter.kt b/vector/src/main/java/im/vector/riotx/core/date/VectorDateFormatter.kt index 367615d765..344398b91e 100644 --- a/vector/src/main/java/im/vector/riotx/core/date/VectorDateFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/core/date/VectorDateFormatter.kt @@ -45,11 +45,12 @@ class VectorDateFormatter @Inject constructor(private val context: Context, if (time == null) { return "" } - return DateUtils.getRelativeDateTimeString(context, - time, - DateUtils.DAY_IN_MILLIS, - 2 * DateUtils.DAY_IN_MILLIS, - DateUtils.FORMAT_SHOW_WEEKDAY + return DateUtils.getRelativeDateTimeString( + context, + time, + DateUtils.DAY_IN_MILLIS, + 2 * DateUtils.DAY_IN_MILLIS, + DateUtils.FORMAT_SHOW_WEEKDAY or DateUtils.FORMAT_SHOW_TIME ).toString() } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 4a767a178e..df6f46b431 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -117,8 +117,10 @@ class RoomDetailViewModel @AssistedInject constructor( // Slot to keep a pending action during permission request var pendingAction: RoomDetailAction? = null + // Slot to keep a pending uri during permission request var pendingUri: Uri? = null + // Slot to store if we want to prevent preview of attachment var preventAttachmentPreview = false @@ -390,7 +392,7 @@ class RoomDetailViewModel @AssistedInject constructor( _viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented) } is ParsedCommand.SendEmote -> { - room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE) + room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE, autoMarkdown = action.autoMarkdown) _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) popDraft() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt index 7ac0c8b1e8..329f66459b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt @@ -27,6 +27,7 @@ import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.message.MessageTextContent import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply +import im.vector.matrix.android.internal.session.room.send.TextContent import im.vector.riotx.R import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.extensions.localDateTime @@ -90,17 +91,15 @@ class ViewEditHistoryEpoxyController(private val context: Context, } lastDate = evDate val cContent = getCorrectContent(timelineEvent, isOriginalReply) - val body = cContent.second?.let { eventHtmlRenderer.render(it) } - ?: cContent.first + val body = cContent.formattedText?.let { eventHtmlRenderer.render(it) } ?: cContent.text val nextEvent = sourceEvents.getOrNull(index + 1) var spannedDiff: Spannable? = null - if (nextEvent != null && cContent.second == null /*No diff for html*/) { + if (nextEvent != null && cContent.formattedText == null /*No diff for html*/) { // compares the body val nContent = getCorrectContent(nextEvent, isOriginalReply) - val nextBody = nContent.second?.let { eventHtmlRenderer.render(it) } - ?: nContent.first + val nextBody = nContent.formattedText?.let { eventHtmlRenderer.render(it) } ?: nContent.text val dmp = diff_match_patch() val diff = dmp.diff_main(nextBody.toString(), body.toString()) dmp.diff_cleanupSemantic(diff) @@ -138,15 +137,14 @@ class ViewEditHistoryEpoxyController(private val context: Context, } } - private fun getCorrectContent(event: Event, isOriginalReply: Boolean): Pair