diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt index a2b38b6606..9c41f0d344 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt @@ -56,6 +56,16 @@ interface SendService { */ fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String = MessageType.MSGTYPE_TEXT): Cancelable + /** + * Method to quote an events content. + * @param quotedEvent The event to which we will quote it's content. + * @param text the text message to send + * @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE + * @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present + * @return a [Cancelable] + */ + fun sendQuotedTextMessage(quotedEvent: TimelineEvent, text: String, autoMarkdown: Boolean = false): Cancelable + /** * Method to send a media asynchronously. * @param attachment the media to send diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt index 77aadef6bd..47cc1fc29a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt @@ -97,6 +97,12 @@ internal class DefaultSendService @AssistedInject constructor( .let { sendEvent(it) } } + override fun sendQuotedTextMessage(quotedEvent: TimelineEvent, text: String, autoMarkdown: Boolean): Cancelable { + return localEchoEventFactory.createQuotedTextEvent(roomId, quotedEvent, text, autoMarkdown) + .also { createLocalEcho(it) } + .let { sendEvent(it) } + } + override fun sendPoll(question: String, options: List): Cancelable { return localEchoEventFactory.createPollEvent(roomId, question, options) .also { createLocalEcho(it) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index 5cb9687518..8c7f96d1fe 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -20,6 +20,7 @@ import android.content.Context import android.graphics.Bitmap import android.media.MediaMetadataRetriever import androidx.exifinterface.media.ExifInterface +import okhttp3.internal.format import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Event @@ -88,9 +89,9 @@ internal class LocalEchoEventFactory @Inject constructor( return createMessageEvent(roomId, content) } - private fun createTextContent(text: CharSequence, autoMarkdown: Boolean): TextContent { + private fun createTextContent(text: CharSequence, autoMarkdown: Boolean, forceMarkdownParse: Boolean = false): TextContent { if (autoMarkdown) { - return markdownParser.parse(text) + return markdownParser.parse(text, force = forceMarkdownParse) } else { // Try to detect pills textPillsUtils.processSpecialSpansToHtml(text)?.let { @@ -175,13 +176,17 @@ internal class LocalEchoEventFactory @Inject constructor( val userLink = originalEvent.root.senderId?.let { permalinkFactory.createPermalink(it, false) } ?: "" val body = bodyForReply(originalEvent.getLastMessageContent(), originalEvent.isReply()) + // As we always supply formatted body for replies we should force the MarkdownParser to produce html. + val newBodyFormatted = createTextContent(newBodyText, newBodyAutoMarkdown, forceMarkdownParse = true).takeFormatted() + // Body of the original message may not have formatted version, so may also have to convert to html. + val bodyFormatted = body.formattedText ?: createTextContent(body.text, newBodyAutoMarkdown, forceMarkdownParse = true).takeFormatted() val replyFormatted = REPLY_PATTERN.format( permalink, userLink, originalEvent.senderInfo.disambiguatedDisplayName, // Remove inner mx_reply tags if any - body.takeFormatted().replace(MX_REPLY_REGEX, ""), - createTextContent(newBodyText, newBodyAutoMarkdown).takeFormatted() + bodyFormatted.replace(MX_REPLY_REGEX, ""), + newBodyFormatted ) // // > <@alice:example.org> This is the original body @@ -361,13 +366,18 @@ internal class LocalEchoEventFactory @Inject constructor( val userLink = permalinkFactory.createPermalink(userId, false) ?: return null val body = bodyForReply(eventReplied.getLastMessageContent(), eventReplied.isReply()) + + // As we always supply formatted body for replies we should force the MarkdownParser to produce html. + val replyTextFormatted = createTextContent(replyText, autoMarkdown, forceMarkdownParse = true).takeFormatted() + // Body of the original message may not have formatted version, so may also have to convert to html. + val bodyFormatted = body.formattedText ?: createTextContent(body.text, autoMarkdown, forceMarkdownParse = true).takeFormatted() val replyFormatted = REPLY_PATTERN.format( permalink, userLink, userId, // Remove inner mx_reply tags if any - body.takeFormatted().replace(MX_REPLY_REGEX, ""), - createTextContent(replyText, autoMarkdown).takeFormatted() + bodyFormatted.replace(MX_REPLY_REGEX, ""), + replyTextFormatted ) // // > <@alice:example.org> This is the original body @@ -467,6 +477,38 @@ internal class LocalEchoEventFactory @Inject constructor( localEchoRepository.createLocalEcho(event) } + fun createQuotedTextEvent( + roomId: String, + quotedEvent: TimelineEvent, + text: String, + autoMarkdown: Boolean + ): Event { + val messageContent = quotedEvent.getLastMessageContent() + val textMsg = messageContent?.body + val quoteText = legacyRiotQuoteText(textMsg, text) + return createFormattedTextEvent(roomId, createTextContent(quoteText, autoMarkdown), MessageType.MSGTYPE_TEXT) + } + + private fun legacyRiotQuoteText(quotedText: String?, myText: String): String { + val messageParagraphs = quotedText?.split("\n\n".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray() + return buildString { + if (messageParagraphs != null) { + for (i in messageParagraphs.indices) { + if (messageParagraphs[i].isNotBlank()) { + append("> ") + append(messageParagraphs[i]) + } + + if (i != messageParagraphs.lastIndex) { + append("\n\n") + } + } + } + append("\n\n") + append(myText) + } + } + companion object { // //
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParser.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParser.kt index c99d482300..2df668bf40 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParser.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParser.kt @@ -34,11 +34,17 @@ internal class MarkdownParser @Inject constructor( private val mdSpecialChars = "[`_\\-*>.\\[\\]#~]".toRegex() - fun parse(text: CharSequence): TextContent { + /** + * Parses some input text and produces html. + * @param text An input CharSequence to be parsed. + * @param force Skips the check for detecting if the input contains markdown and always converts to html. + * @return TextContent containing the plain text and the formatted html if generated. + */ + fun parse(text: CharSequence, force: Boolean = false): TextContent { val source = textPillsUtils.processSpecialSpansToMarkdown(text) ?: text.toString() // If no special char are detected, just return plain text - if (source.contains(mdSpecialChars).not()) { + if (!force && source.contains(mdSpecialChars).not()) { return TextContent(source) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt index 66d49f9819..55a7eba0de 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt @@ -395,23 +395,7 @@ class TextComposerViewModel @AssistedInject constructor( popDraft() } is SendMode.QUOTE -> { - val messageContent = state.sendMode.timelineEvent.getLastMessageContent() - val textMsg = messageContent?.body - - val finalText = legacyRiotQuoteText(textMsg, action.text.toString()) - - // TODO check for pills? - - // TODO Refactor this, just temporary for quotes - val parser = Parser.builder().build() - val document = parser.parse(finalText) - val renderer = HtmlRenderer.builder().build() - val htmlText = renderer.render(document) - if (finalText == htmlText) { - room.sendTextMessage(finalText) - } else { - room.sendFormattedTextMessage(finalText, htmlText) - } + room.sendQuotedTextMessage(state.sendMode.timelineEvent, action.text.toString(), action.autoMarkdown) _viewEvents.post(TextComposerViewEvents.MessageSent) popDraft() }