diff --git a/changelog.d/7975.bugfix b/changelog.d/7975.bugfix
new file mode 100644
index 0000000000..b34c784b27
--- /dev/null
+++ b/changelog.d/7975.bugfix
@@ -0,0 +1 @@
+Fix extra new lines added to inline code
diff --git a/changelog.d/8011.feature b/changelog.d/8011.feature
new file mode 100644
index 0000000000..700a528fc1
--- /dev/null
+++ b/changelog.d/8011.feature
@@ -0,0 +1 @@
+[Rich text editor] Add inline code to rich text editor
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index 46c175437a..e690f06bbb 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -3502,6 +3502,7 @@
Set link
Toggle numbered list
Toggle bullet list
+ Apply inline code format
Toggle full screen mode
Text
diff --git a/vector/src/androidTest/java/im/vector/app/core/utils/TestSpan.kt b/vector/src/androidTest/java/im/vector/app/core/utils/TestSpan.kt
index e31dc6942c..1d0d6548e1 100644
--- a/vector/src/androidTest/java/im/vector/app/core/utils/TestSpan.kt
+++ b/vector/src/androidTest/java/im/vector/app/core/utils/TestSpan.kt
@@ -19,9 +19,10 @@ package im.vector.app.core.utils
import android.graphics.Canvas
import android.graphics.Paint
import android.text.Layout
-import android.text.Spannable
+import android.text.Spanned
import androidx.core.text.getSpans
import im.vector.app.features.html.HtmlCodeSpan
+import io.element.android.wysiwyg.spans.InlineCodeSpan
import io.mockk.justRun
import io.mockk.mockk
import io.mockk.slot
@@ -31,9 +32,9 @@ import io.noties.markwon.core.spans.OrderedListItemSpan
import io.noties.markwon.core.spans.StrongEmphasisSpan
import me.gujun.android.span.style.CustomTypefaceSpan
-fun Spannable.toTestSpan(): String {
+fun Spanned.toTestSpan(): String {
var output = toString()
- readSpansWithContent().forEach {
+ readSpansWithContent().reversed().forEach {
val tags = it.span.readTags()
val remappedContent = it.span.remapContent(source = this, originalContent = it.content)
output = output.replace(it.content, "${tags.open}$remappedContent${tags.close}")
@@ -41,7 +42,7 @@ fun Spannable.toTestSpan(): String {
return output
}
-private fun Spannable.readSpansWithContent() = getSpans().map { span ->
+private fun Spanned.readSpansWithContent() = getSpans().map { span ->
val start = getSpanStart(span)
val end = getSpanEnd(span)
SpanWithContent(
@@ -51,12 +52,24 @@ private fun Spannable.readSpansWithContent() = getSpans().map { span ->
}.reversed()
private fun Any.readTags(): SpanTags {
- return when (this::class) {
- OrderedListItemSpan::class -> SpanTags("[list item]", "[/list item]")
- HtmlCodeSpan::class -> SpanTags("[code]", "[/code]")
- StrongEmphasisSpan::class -> SpanTags("[bold]", "[/bold]")
- EmphasisSpan::class, CustomTypefaceSpan::class -> SpanTags("[italic]", "[/italic]")
- else -> throw IllegalArgumentException("Unknown ${this::class}")
+ val tagName = when (this::class) {
+ OrderedListItemSpan::class -> "list item"
+ HtmlCodeSpan::class ->
+ if ((this as HtmlCodeSpan).isBlock) "code block" else "inline code"
+ StrongEmphasisSpan::class -> "bold"
+ EmphasisSpan::class, CustomTypefaceSpan::class -> "italic"
+ InlineCodeSpan::class -> "inline code"
+ else -> if (this::class.qualifiedName!!.startsWith("android.widget")) {
+ null
+ } else {
+ throw IllegalArgumentException("Unknown ${this::class}")
+ }
+ }
+
+ return if (tagName == null) {
+ SpanTags("", "")
+ } else {
+ SpanTags("[$tagName]", "[/$tagName]")
}
}
diff --git a/vector/src/androidTest/java/im/vector/app/features/html/EventHtmlRendererTest.kt b/vector/src/androidTest/java/im/vector/app/features/html/EventHtmlRendererTest.kt
index a2e489dd70..7f3293e7d1 100644
--- a/vector/src/androidTest/java/im/vector/app/features/html/EventHtmlRendererTest.kt
+++ b/vector/src/androidTest/java/im/vector/app/features/html/EventHtmlRendererTest.kt
@@ -16,7 +16,8 @@
package im.vector.app.features.html
-import androidx.core.text.toSpannable
+import android.widget.TextView
+import androidx.core.text.toSpanned
import androidx.test.platform.app.InstrumentationRegistry
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.resources.ColorProvider
@@ -36,16 +37,19 @@ class EventHtmlRendererTest {
private val context = InstrumentationRegistry.getInstrumentation().targetContext
private val fakeVectorPreferences = mockk().also {
every { it.latexMathsIsEnabled() } returns false
+ every { it.isRichTextEditorEnabled() } returns false
}
private val fakeSessionHolder = mockk()
private val renderer = EventHtmlRenderer(
- MatrixHtmlPluginConfigure(ColorProvider(context), context.resources),
+ MatrixHtmlPluginConfigure(ColorProvider(context), context.resources, fakeVectorPreferences),
context,
fakeVectorPreferences,
fakeSessionHolder,
)
+ private val textView: TextView = TextView(context)
+
@Test
fun takesInitialListPositionIntoAccount() {
val result = """- first entry
""".renderAsTestSpan()
@@ -57,7 +61,7 @@ class EventHtmlRendererTest {
fun doesNotProcessMarkdownWithinCodeBlocks() {
val result = """__italic__ **bold**
""".renderAsTestSpan()
- result shouldBeEqualTo "[code]__italic__ **bold**[/code]"
+ result shouldBeEqualTo "[inline code]__italic__ **bold**[/inline code]"
}
@Test
@@ -71,7 +75,15 @@ class EventHtmlRendererTest {
fun processesHtmlWithinCodeBlocks() {
val result = """italic bold
""".renderAsTestSpan()
- result shouldBeEqualTo "[code][italic]italic[/italic] [bold]bold[/bold][/code]"
+ result shouldBeEqualTo "[inline code][italic]italic[/italic] [bold]bold[/bold][/inline code]"
+ }
+
+ @Test
+ fun processesHtmlWithinCodeBlocks_givenRichTextEditorEnabled() {
+ every { fakeVectorPreferences.isRichTextEditorEnabled() } returns true
+ val result = """italic bold
""".renderAsTestSpan()
+
+ result shouldBeEqualTo "[inline code][italic]italic[/italic] [bold]bold[/bold][/inline code]"
}
@Test
@@ -81,5 +93,9 @@ class EventHtmlRendererTest {
result shouldBeEqualTo """& < > ' """"
}
- private fun String.renderAsTestSpan() = renderer.render(this).toSpannable().toTestSpan()
+ private fun String.renderAsTestSpan(): String {
+ textView.text = renderer.render(this).toSpanned()
+ renderer.plugins.forEach { markwonPlugin -> markwonPlugin.afterSetText(textView) }
+ return textView.text.toSpanned().toTestSpan()
+ }
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt
index 1bb82b41fe..2c0d77045e 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt
@@ -246,6 +246,9 @@ internal class RichTextComposerLayout @JvmOverloads constructor(
addRichTextMenuItem(R.drawable.ic_composer_numbered_list, R.string.rich_text_editor_numbered_list, ComposerAction.ORDERED_LIST) {
views.richTextComposerEditText.toggleList(ordered = true)
}
+ addRichTextMenuItem(R.drawable.ic_composer_inline_code, R.string.rich_text_editor_inline_code, ComposerAction.INLINE_CODE) {
+ views.richTextComposerEditText.toggleInlineFormat(InlineFormat.InlineCode)
+ }
}
fun setLink(link: String?) =
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
index 219ccbe11c..9cb1608415 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
@@ -160,6 +160,9 @@ class MessageItemFactory @Inject constructor(
textRendererFactory.create(roomId)
}
+ private val useRichTextEditorStyle: Boolean get() =
+ vectorPreferences.isRichTextEditorEnabled()
+
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
val event = params.event
val highlight = params.isHighlighted
@@ -480,6 +483,7 @@ class MessageItemFactory @Inject constructor(
highlight,
callback,
attributes,
+ useRichTextEditorStyle = vectorPreferences.isRichTextEditorEnabled(),
)
}
@@ -586,7 +590,7 @@ class MessageItemFactory @Inject constructor(
val replyToContent = messageContent.relatesTo?.inReplyTo
buildFormattedTextItem(matrixFormattedBody, informationData, highlight, callback, attributes, replyToContent)
} else {
- buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes)
+ buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes, useRichTextEditorStyle)
}
}
@@ -610,6 +614,7 @@ class MessageItemFactory @Inject constructor(
highlight,
callback,
attributes,
+ useRichTextEditorStyle,
)
}
@@ -620,6 +625,7 @@ class MessageItemFactory @Inject constructor(
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes,
+ useRichTextEditorStyle: Boolean,
): MessageTextItem? {
val renderedBody = textRenderer.render(body)
val bindingOptions = spanUtils.getBindingOptions(renderedBody)
@@ -640,6 +646,7 @@ class MessageItemFactory @Inject constructor(
.previewUrlRetriever(callback?.getPreviewUrlRetriever())
.imageContentRenderer(imageContentRenderer)
.previewUrlCallback(callback)
+ .useRichTextEditorStyle(useRichTextEditorStyle)
.leftGuideline(avatarSizeProvider.leftGuideline)
.attributes(attributes)
.highlighted(highlight)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt
index 072c3dcd27..a9cd25ae19 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt
@@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail.timeline.item
import android.text.Spanned
import android.text.method.MovementMethod
+import android.view.ViewStub
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.text.PrecomputedTextCompat
import androidx.core.view.isVisible
@@ -67,6 +68,9 @@ abstract class MessageTextItem : AbsMessageItem() {
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
var markwonPlugins: (List)? = null
+ @EpoxyAttribute
+ var useRichTextEditorStyle: Boolean = false
+
private val previewUrlViewUpdater = PreviewUrlViewUpdater()
override fun bind(holder: Holder) {
@@ -82,27 +86,28 @@ abstract class MessageTextItem : AbsMessageItem() {
holder.previewUrlView.delegate = previewUrlCallback
holder.previewUrlView.renderMessageLayout(attributes.informationData.messageLayout)
+ val messageView: AppCompatTextView = if (useRichTextEditorStyle) holder.richMessageView else holder.plainMessageView
if (useBigFont) {
- holder.messageView.textSize = 44F
+ messageView.textSize = 44F
} else {
- holder.messageView.textSize = 15.5F
+ messageView.textSize = 15.5F
}
if (searchForPills) {
message?.charSequence?.findPillsAndProcess(coroutineScope) {
// mmm.. not sure this is so safe in regards to cell reuse
- it.bind(holder.messageView)
+ it.bind(messageView)
}
}
message?.charSequence.let { charSequence ->
- markwonPlugins?.forEach { plugin -> plugin.beforeSetText(holder.messageView, charSequence as Spanned) }
+ markwonPlugins?.forEach { plugin -> plugin.beforeSetText(messageView, charSequence as Spanned) }
}
super.bind(holder)
- holder.messageView.movementMethod = movementMethod
- renderSendState(holder.messageView, holder.messageView)
- holder.messageView.onClick(attributes.itemClickListener)
- holder.messageView.onLongClickIgnoringLinks(attributes.itemLongClickListener)
- holder.messageView.setTextWithEmojiSupport(message?.charSequence, bindingOptions)
- markwonPlugins?.forEach { plugin -> plugin.afterSetText(holder.messageView) }
+ messageView.movementMethod = movementMethod
+ renderSendState(messageView, messageView)
+ messageView.onClick(attributes.itemClickListener)
+ messageView.onLongClickIgnoringLinks(attributes.itemLongClickListener)
+ messageView.setTextWithEmojiSupport(message?.charSequence, bindingOptions)
+ markwonPlugins?.forEach { plugin -> plugin.afterSetText(messageView) }
}
private fun AppCompatTextView.setTextWithEmojiSupport(message: CharSequence?, bindingOptions: BindingOptions?) {
@@ -125,8 +130,15 @@ abstract class MessageTextItem : AbsMessageItem() {
override fun getViewStubId() = STUB_ID
class Holder : AbsMessageItem.Holder(STUB_ID) {
- val messageView by bind(R.id.messageTextView)
val previewUrlView by bind(R.id.messageUrlPreview)
+ private val richMessageStub by bind(R.id.richMessageTextViewStub)
+ private val plainMessageStub by bind(R.id.plainMessageTextViewStub)
+ val richMessageView: AppCompatTextView by lazy {
+ richMessageStub.inflate().findViewById(R.id.messageTextView)
+ }
+ val plainMessageView: AppCompatTextView by lazy {
+ plainMessageStub.inflate().findViewById(R.id.messageTextView)
+ }
}
inner class PreviewUrlViewUpdater : PreviewUrlRetriever.PreviewUrlRetrieverListener {
diff --git a/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt b/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt
index 21fcbffb03..bc9ba0b85a 100644
--- a/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt
+++ b/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt
@@ -30,6 +30,8 @@ import android.content.res.Resources
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.text.Spannable
+import android.text.SpannableStringBuilder
+import android.widget.TextView
import androidx.core.text.toSpannable
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestBuilder
@@ -38,6 +40,7 @@ import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.settings.VectorPreferences
+import io.element.android.wysiwyg.spans.InlineCodeSpan
import io.noties.markwon.AbstractMarkwonPlugin
import io.noties.markwon.Markwon
import io.noties.markwon.MarkwonPlugin
@@ -64,8 +67,8 @@ import javax.inject.Singleton
@Singleton
class EventHtmlRenderer @Inject constructor(
htmlConfigure: MatrixHtmlPluginConfigure,
- context: Context,
- vectorPreferences: VectorPreferences,
+ private val context: Context,
+ private val vectorPreferences: VectorPreferences,
private val activeSessionHolder: ActiveSessionHolder
) {
@@ -73,73 +76,121 @@ class EventHtmlRenderer @Inject constructor(
fun afterRender(renderedText: Spannable)
}
- private val builder = Markwon.builder(context)
- .usePlugin(HtmlPlugin.create(htmlConfigure))
- .usePlugin(GlideImagesPlugin.create(object : GlideImagesPlugin.GlideStore {
- override fun load(drawable: AsyncDrawable): RequestBuilder {
- val url = drawable.destination
- if (url.isMxcUrl()) {
- val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
- val imageUrl = contentUrlResolver.resolveFullSize(url)
- // Override size to avoid crashes for huge pictures
- return Glide.with(context).load(imageUrl).override(500)
- }
- // We don't want to support other url schemes here, so just return a request for null
- return Glide.with(context).load(null as String?)
- }
+ private val glidePlugin = GlideImagesPlugin.create(object : GlideImagesPlugin.GlideStore {
+ override fun load(drawable: AsyncDrawable): RequestBuilder {
+ val url = drawable.destination
+ if (url.isMxcUrl()) {
+ val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
+ val imageUrl = contentUrlResolver.resolveFullSize(url)
+ // Override size to avoid crashes for huge pictures
+ return Glide.with(context).load(imageUrl).override(500)
+ }
+ // We don't want to support other url schemes here, so just return a request for null
+ return Glide.with(context).load(null as String?)
+ }
- override fun cancel(target: Target<*>) {
- Glide.with(context).clear(target)
- }
- }))
+ override fun cancel(target: Target<*>) {
+ Glide.with(context).clear(target)
+ }
+ })
- private val markwon = if (vectorPreferences.latexMathsIsEnabled()) {
- // If latex maths is enabled in app preferences, refomat it so Markwon recognises it
- // It needs to be in this specific format: https://noties.io/Markwon/docs/v4/ext-latex
- builder
- .usePlugin(object : AbstractMarkwonPlugin() {
- override fun processMarkdown(markdown: String): String {
- return markdown
- .replace(Regex(""".*?""")) { matchResult ->
- "$$" + matchResult.groupValues[1] + "$$"
- }
- .replace(Regex(""".*?
""")) { matchResult ->
- "\n$$\n" + matchResult.groupValues[1] + "\n$$\n"
- }
- }
- })
- .usePlugin(JLatexMathPlugin.create(44F) { builder ->
- builder.inlinesEnabled(true)
- builder.theme().inlinePadding(JLatexMathTheme.Padding.symmetric(24, 8))
- })
- } else {
- builder
- }
- .usePlugin(
- MarkwonInlineParserPlugin.create(
- /* Configuring the Markwon inline formatting processor.
- * Default settings are all Markdown features. Turn those off, only using the
- * inline HTML processor and HTML entities processor.
- */
- MarkwonInlineParser.factoryBuilderNoDefaults()
- .addInlineProcessor(HtmlInlineProcessor()) // use inline HTML processor
- .addInlineProcessor(EntityInlineProcessor()) // use HTML entities processor
- )
- )
- .usePlugin(object : AbstractMarkwonPlugin() {
- override fun configureSpansFactory(builder: MarkwonSpansFactory.Builder) {
- builder.setFactory(
- Emphasis::class.java
- ) { _, _ -> CustomTypefaceSpan(Typeface.create(Typeface.DEFAULT, Typeface.ITALIC)) }
+ private val latexPlugins = listOf(
+ object : AbstractMarkwonPlugin() {
+ override fun processMarkdown(markdown: String): String {
+ return markdown
+ .replace(Regex(""".*?""")) { matchResult ->
+ "$$" + matchResult.groupValues[1] + "$$"
+ }
+ .replace(Regex(""".*?
""")) { matchResult ->
+ "\n$$\n" + matchResult.groupValues[1] + "\n$$\n"
+ }
}
+ },
+ JLatexMathPlugin.create(44F) { builder ->
+ builder.inlinesEnabled(true)
+ builder.theme().inlinePadding(JLatexMathTheme.Padding.symmetric(24, 8))
+ }
+ )
- override fun configureParser(builder: Parser.Builder) {
- /* Configuring the Markwon block formatting processor.
- * Default settings are all Markdown blocks. Turn those off.
+ private val markwonInlineParserPlugin =
+ MarkwonInlineParserPlugin.create(
+ /* Configuring the Markwon inline formatting processor.
+ * Default settings are all Markdown features. Turn those off, only using the
+ * inline HTML processor and HTML entities processor.
*/
- builder.enabledBlockTypes(kotlin.collections.emptySet())
+ MarkwonInlineParser.factoryBuilderNoDefaults()
+ .addInlineProcessor(HtmlInlineProcessor()) // use inline HTML processor
+ .addInlineProcessor(EntityInlineProcessor()) // use HTML entities processor
+ )
+
+ private val italicPlugin = object : AbstractMarkwonPlugin() {
+ override fun configureSpansFactory(builder: MarkwonSpansFactory.Builder) {
+ builder.setFactory(
+ Emphasis::class.java
+ ) { _, _ -> CustomTypefaceSpan(Typeface.create(Typeface.DEFAULT, Typeface.ITALIC)) }
+ }
+
+ override fun configureParser(builder: Parser.Builder) {
+ /* Configuring the Markwon block formatting processor.
+ * Default settings are all Markdown blocks. Turn those off.
+ */
+ builder.enabledBlockTypes(emptySet())
+ }
+ }
+
+ private val cleanUpIntermediateCodePlugin = object : AbstractMarkwonPlugin() {
+ override fun afterSetText(textView: TextView) {
+ super.afterSetText(textView)
+
+ // Remove any intermediate spans
+ val text = textView.text.toSpannable()
+ text.getSpans(0, text.length, IntermediateCodeSpan::class.java)
+ .forEach { span ->
+ text.removeSpan(span)
+ }
+ }
+ }
+
+ /**
+ * Workaround for https://github.com/noties/Markwon/issues/423
+ */
+ private val removeLeadingNewlineForInlineCode = object : AbstractMarkwonPlugin() {
+ override fun afterSetText(textView: TextView) {
+ super.afterSetText(textView)
+
+ val text = SpannableStringBuilder(textView.text.toSpannable())
+ val inlineCodeSpans = text.getSpans(0, textView.length(), InlineCodeSpan::class.java).toList()
+ val legacyInlineCodeSpans = text.getSpans(0, textView.length(), HtmlCodeSpan::class.java).filter { !it.isBlock }
+ val spans = inlineCodeSpans + legacyInlineCodeSpans
+
+ if (spans.isEmpty()) return
+
+ spans.forEach { span ->
+ val start = text.getSpanStart(span)
+ if (text[start] == '\n') {
+ text.replace(start, start + 1, "")
}
- })
+ }
+
+ textView.text = text
+ }
+ }
+
+ private val markwon = Markwon.builder(context)
+ .usePlugin(HtmlRootTagPlugin())
+ .usePlugin(HtmlPlugin.create(htmlConfigure))
+ .usePlugin(removeLeadingNewlineForInlineCode)
+ .usePlugin(glidePlugin)
+ .apply {
+ if (vectorPreferences.latexMathsIsEnabled()) {
+ // If latex maths is enabled in app preferences, refomat it so Markwon recognises it
+ // It needs to be in this specific format: https://noties.io/Markwon/docs/v4/ext-latex
+ latexPlugins.forEach(::usePlugin)
+ }
+ }
+ .usePlugin(markwonInlineParserPlugin)
+ .usePlugin(italicPlugin)
+ .usePlugin(cleanUpIntermediateCodePlugin)
.textSetter(PrecomputedFutureTextSetterCompat.create())
.build()
@@ -185,7 +236,11 @@ class EventHtmlRenderer @Inject constructor(
}
}
-class MatrixHtmlPluginConfigure @Inject constructor(private val colorProvider: ColorProvider, private val resources: Resources) : HtmlPlugin.HtmlConfigure {
+class MatrixHtmlPluginConfigure @Inject constructor(
+ private val colorProvider: ColorProvider,
+ private val resources: Resources,
+ private val vectorPreferences: VectorPreferences,
+) : HtmlPlugin.HtmlConfigure {
override fun configureHtml(plugin: HtmlPlugin) {
plugin
@@ -193,6 +248,7 @@ class MatrixHtmlPluginConfigure @Inject constructor(private val colorProvider: C
.addHandler(FontTagHandler())
.addHandler(ParagraphHandler(DimensionConverter(resources)))
.addHandler(MxReplyTagHandler())
+ .addHandler(CodePostProcessorTagHandler(vectorPreferences))
.addHandler(CodePreTagHandler())
.addHandler(CodeTagHandler())
.addHandler(SpanHandler(colorProvider))
diff --git a/vector/src/main/java/im/vector/app/features/html/HtmlCodeHandlers.kt b/vector/src/main/java/im/vector/app/features/html/HtmlCodeHandlers.kt
index 1010625370..295b74c7a9 100644
--- a/vector/src/main/java/im/vector/app/features/html/HtmlCodeHandlers.kt
+++ b/vector/src/main/java/im/vector/app/features/html/HtmlCodeHandlers.kt
@@ -16,20 +16,29 @@
package im.vector.app.features.html
+import im.vector.app.features.settings.VectorPreferences
+import io.element.android.wysiwyg.spans.InlineCodeSpan
import io.noties.markwon.MarkwonVisitor
import io.noties.markwon.SpannableBuilder
+import io.noties.markwon.core.MarkwonTheme
import io.noties.markwon.html.HtmlTag
import io.noties.markwon.html.MarkwonHtmlRenderer
import io.noties.markwon.html.TagHandler
-class CodeTagHandler : TagHandler() {
+/**
+ * Span to be added to any found during initial pass.
+ * The actual code spans can then be added on a second pass using this
+ * span as a reference.
+ */
+internal class IntermediateCodeSpan(
+ var isBlock: Boolean
+)
+
+internal class CodeTagHandler : TagHandler() {
override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) {
SpannableBuilder.setSpans(
- visitor.builder(),
- HtmlCodeSpan(visitor.configuration().theme(), false),
- tag.start(),
- tag.end()
+ visitor.builder(), IntermediateCodeSpan(isBlock = false), tag.start(), tag.end()
)
}
@@ -42,15 +51,13 @@ class CodeTagHandler : TagHandler() {
* Pre tag are already handled by HtmlPlugin to keep the formatting.
* We are only using it to check for *
tags.
*/
-class CodePreTagHandler : TagHandler() {
+internal class CodePreTagHandler : TagHandler() {
override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) {
- val htmlCodeSpan = visitor.builder()
- .getSpans(tag.start(), tag.end())
- .firstOrNull {
- it.what is HtmlCodeSpan
- }
- if (htmlCodeSpan != null) {
- (htmlCodeSpan.what as HtmlCodeSpan).isBlock = true
+ val codeSpan = visitor.builder().getSpans(tag.start(), tag.end()).firstOrNull {
+ it.what is IntermediateCodeSpan
+ }
+ if (codeSpan != null) {
+ (codeSpan.what as IntermediateCodeSpan).isBlock = true
}
}
@@ -58,3 +65,42 @@ class CodePreTagHandler : TagHandler() {
return listOf("pre")
}
}
+
+internal class CodePostProcessorTagHandler(
+ private val vectorPreferences: VectorPreferences,
+) : TagHandler() {
+
+ override fun supportedTags() = listOf(HtmlRootTagPlugin.ROOT_TAG_NAME)
+
+ override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) {
+ if (tag.attributes()[HtmlRootTagPlugin.ROOT_ATTRIBUTE] == null) {
+ return
+ }
+
+ if (tag.isBlock) {
+ visitChildren(visitor, renderer, tag.asBlock)
+ }
+
+ // Replace any intermediate code spans with the real formatting spans
+ visitor.builder()
+ .getSpans(tag.start(), tag.end())
+ .filter {
+ it.what is IntermediateCodeSpan
+ }.forEach { code ->
+ val intermediateCodeSpan = code.what as IntermediateCodeSpan
+ val theme = visitor.configuration().theme()
+ val span = intermediateCodeSpan.toFinalCodeSpan(theme)
+ SpannableBuilder.setSpans(
+ visitor.builder(), span, code.start, code.end
+ )
+ }
+ }
+
+ private fun IntermediateCodeSpan.toFinalCodeSpan(
+ markwonTheme: MarkwonTheme
+ ): Any = if (vectorPreferences.isRichTextEditorEnabled() && !isBlock) {
+ InlineCodeSpan()
+ } else {
+ HtmlCodeSpan(markwonTheme, isBlock)
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/html/HtmlRootTagPlugin.kt b/vector/src/main/java/im/vector/app/features/html/HtmlRootTagPlugin.kt
new file mode 100644
index 0000000000..59f2cda00b
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/html/HtmlRootTagPlugin.kt
@@ -0,0 +1,33 @@
+/*
+ * 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 im.vector.app.features.html
+
+import io.noties.markwon.AbstractMarkwonPlugin
+
+/**
+ * A root node enables post-processing of optionally nested tags.
+ * See: [im.vector.app.features.html.CodePostProcessorTagHandler]
+ */
+internal class HtmlRootTagPlugin : AbstractMarkwonPlugin() {
+ companion object {
+ const val ROOT_ATTRIBUTE = "data-root"
+ const val ROOT_TAG_NAME = "div"
+ }
+ override fun processMarkdown(html: String): String {
+ return "<$ROOT_TAG_NAME $ROOT_ATTRIBUTE>$html$ROOT_TAG_NAME>"
+ }
+}
diff --git a/vector/src/main/res/drawable/ic_composer_inline_code.xml b/vector/src/main/res/drawable/ic_composer_inline_code.xml
new file mode 100644
index 0000000000..1743b757af
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_composer_inline_code.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/vector/src/main/res/layout/item_timeline_event_text_message_plain_stub.xml b/vector/src/main/res/layout/item_timeline_event_text_message_plain_stub.xml
new file mode 100644
index 0000000000..1d94632686
--- /dev/null
+++ b/vector/src/main/res/layout/item_timeline_event_text_message_plain_stub.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/vector/src/main/res/layout/item_timeline_event_text_message_rich_stub.xml b/vector/src/main/res/layout/item_timeline_event_text_message_rich_stub.xml
new file mode 100644
index 0000000000..bedff8bd4a
--- /dev/null
+++ b/vector/src/main/res/layout/item_timeline_event_text_message_rich_stub.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/vector/src/main/res/layout/item_timeline_event_text_message_stub.xml b/vector/src/main/res/layout/item_timeline_event_text_message_stub.xml
index 5c5280ad4e..32785a41af 100644
--- a/vector/src/main/res/layout/item_timeline_event_text_message_stub.xml
+++ b/vector/src/main/res/layout/item_timeline_event_text_message_stub.xml
@@ -7,14 +7,17 @@
android:orientation="vertical"
tools:viewBindingIgnore="true">
-
+ android:layout="@layout/item_timeline_event_text_message_plain_stub" />
+
+