Skip to content

Commit 00f9c36

Browse files
authored
[Rich text editor] Add inline code to rich text editor (#8011)
Also: - Fixes #7975 - See noties/Markwon#423
1 parent 156f4f7 commit 00f9c36

File tree

15 files changed

+338
-109
lines changed

15 files changed

+338
-109
lines changed

changelog.d/7975.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix extra new lines added to inline code

changelog.d/8011.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[Rich text editor] Add inline code to rich text editor

library/ui-strings/src/main/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3502,6 +3502,7 @@
35023502
<string name="rich_text_editor_link">Set link</string>
35033503
<string name="rich_text_editor_numbered_list">Toggle numbered list</string>
35043504
<string name="rich_text_editor_bullet_list">Toggle bullet list</string>
3505+
<string name="rich_text_editor_inline_code">Apply inline code format</string>
35053506
<string name="rich_text_editor_full_screen_toggle">Toggle full screen mode</string>
35063507

35073508
<string name="set_link_text">Text</string>

vector/src/androidTest/java/im/vector/app/core/utils/TestSpan.kt

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@ package im.vector.app.core.utils
1919
import android.graphics.Canvas
2020
import android.graphics.Paint
2121
import android.text.Layout
22-
import android.text.Spannable
22+
import android.text.Spanned
2323
import androidx.core.text.getSpans
2424
import im.vector.app.features.html.HtmlCodeSpan
25+
import io.element.android.wysiwyg.spans.InlineCodeSpan
2526
import io.mockk.justRun
2627
import io.mockk.mockk
2728
import io.mockk.slot
@@ -31,17 +32,17 @@ import io.noties.markwon.core.spans.OrderedListItemSpan
3132
import io.noties.markwon.core.spans.StrongEmphasisSpan
3233
import me.gujun.android.span.style.CustomTypefaceSpan
3334

34-
fun Spannable.toTestSpan(): String {
35+
fun Spanned.toTestSpan(): String {
3536
var output = toString()
36-
readSpansWithContent().forEach {
37+
readSpansWithContent().reversed().forEach {
3738
val tags = it.span.readTags()
3839
val remappedContent = it.span.remapContent(source = this, originalContent = it.content)
3940
output = output.replace(it.content, "${tags.open}$remappedContent${tags.close}")
4041
}
4142
return output
4243
}
4344

44-
private fun Spannable.readSpansWithContent() = getSpans<Any>().map { span ->
45+
private fun Spanned.readSpansWithContent() = getSpans<Any>().map { span ->
4546
val start = getSpanStart(span)
4647
val end = getSpanEnd(span)
4748
SpanWithContent(
@@ -51,12 +52,24 @@ private fun Spannable.readSpansWithContent() = getSpans<Any>().map { span ->
5152
}.reversed()
5253

5354
private fun Any.readTags(): SpanTags {
54-
return when (this::class) {
55-
OrderedListItemSpan::class -> SpanTags("[list item]", "[/list item]")
56-
HtmlCodeSpan::class -> SpanTags("[code]", "[/code]")
57-
StrongEmphasisSpan::class -> SpanTags("[bold]", "[/bold]")
58-
EmphasisSpan::class, CustomTypefaceSpan::class -> SpanTags("[italic]", "[/italic]")
59-
else -> throw IllegalArgumentException("Unknown ${this::class}")
55+
val tagName = when (this::class) {
56+
OrderedListItemSpan::class -> "list item"
57+
HtmlCodeSpan::class ->
58+
if ((this as HtmlCodeSpan).isBlock) "code block" else "inline code"
59+
StrongEmphasisSpan::class -> "bold"
60+
EmphasisSpan::class, CustomTypefaceSpan::class -> "italic"
61+
InlineCodeSpan::class -> "inline code"
62+
else -> if (this::class.qualifiedName!!.startsWith("android.widget")) {
63+
null
64+
} else {
65+
throw IllegalArgumentException("Unknown ${this::class}")
66+
}
67+
}
68+
69+
return if (tagName == null) {
70+
SpanTags("", "")
71+
} else {
72+
SpanTags("[$tagName]", "[/$tagName]")
6073
}
6174
}
6275

vector/src/androidTest/java/im/vector/app/features/html/EventHtmlRendererTest.kt

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616

1717
package im.vector.app.features.html
1818

19-
import androidx.core.text.toSpannable
19+
import android.widget.TextView
20+
import androidx.core.text.toSpanned
2021
import androidx.test.platform.app.InstrumentationRegistry
2122
import im.vector.app.core.di.ActiveSessionHolder
2223
import im.vector.app.core.resources.ColorProvider
@@ -36,16 +37,19 @@ class EventHtmlRendererTest {
3637
private val context = InstrumentationRegistry.getInstrumentation().targetContext
3738
private val fakeVectorPreferences = mockk<VectorPreferences>().also {
3839
every { it.latexMathsIsEnabled() } returns false
40+
every { it.isRichTextEditorEnabled() } returns false
3941
}
4042
private val fakeSessionHolder = mockk<ActiveSessionHolder>()
4143

4244
private val renderer = EventHtmlRenderer(
43-
MatrixHtmlPluginConfigure(ColorProvider(context), context.resources),
45+
MatrixHtmlPluginConfigure(ColorProvider(context), context.resources, fakeVectorPreferences),
4446
context,
4547
fakeVectorPreferences,
4648
fakeSessionHolder,
4749
)
4850

51+
private val textView: TextView = TextView(context)
52+
4953
@Test
5054
fun takesInitialListPositionIntoAccount() {
5155
val result = """<ol start="5"><li>first entry<li></ol>""".renderAsTestSpan()
@@ -57,7 +61,7 @@ class EventHtmlRendererTest {
5761
fun doesNotProcessMarkdownWithinCodeBlocks() {
5862
val result = """<code>__italic__ **bold**</code>""".renderAsTestSpan()
5963

60-
result shouldBeEqualTo "[code]__italic__ **bold**[/code]"
64+
result shouldBeEqualTo "[inline code]__italic__ **bold**[/inline code]"
6165
}
6266

6367
@Test
@@ -71,7 +75,15 @@ class EventHtmlRendererTest {
7175
fun processesHtmlWithinCodeBlocks() {
7276
val result = """<code><i>italic</i> <b>bold</b></code>""".renderAsTestSpan()
7377

74-
result shouldBeEqualTo "[code][italic]italic[/italic] [bold]bold[/bold][/code]"
78+
result shouldBeEqualTo "[inline code][italic]italic[/italic] [bold]bold[/bold][/inline code]"
79+
}
80+
81+
@Test
82+
fun processesHtmlWithinCodeBlocks_givenRichTextEditorEnabled() {
83+
every { fakeVectorPreferences.isRichTextEditorEnabled() } returns true
84+
val result = """<code><i>italic</i> <b>bold</b></code>""".renderAsTestSpan()
85+
86+
result shouldBeEqualTo "[inline code][italic]italic[/italic] [bold]bold[/bold][/inline code]"
7587
}
7688

7789
@Test
@@ -81,5 +93,9 @@ class EventHtmlRendererTest {
8193
result shouldBeEqualTo """& < > ' """"
8294
}
8395

84-
private fun String.renderAsTestSpan() = renderer.render(this).toSpannable().toTestSpan()
96+
private fun String.renderAsTestSpan(): String {
97+
textView.text = renderer.render(this).toSpanned()
98+
renderer.plugins.forEach { markwonPlugin -> markwonPlugin.afterSetText(textView) }
99+
return textView.text.toSpanned().toTestSpan()
100+
}
85101
}

vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,9 @@ internal class RichTextComposerLayout @JvmOverloads constructor(
246246
addRichTextMenuItem(R.drawable.ic_composer_numbered_list, R.string.rich_text_editor_numbered_list, ComposerAction.ORDERED_LIST) {
247247
views.richTextComposerEditText.toggleList(ordered = true)
248248
}
249+
addRichTextMenuItem(R.drawable.ic_composer_inline_code, R.string.rich_text_editor_inline_code, ComposerAction.INLINE_CODE) {
250+
views.richTextComposerEditText.toggleInlineFormat(InlineFormat.InlineCode)
251+
}
249252
}
250253

251254
fun setLink(link: String?) =

vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,9 @@ class MessageItemFactory @Inject constructor(
160160
textRendererFactory.create(roomId)
161161
}
162162

163+
private val useRichTextEditorStyle: Boolean get() =
164+
vectorPreferences.isRichTextEditorEnabled()
165+
163166
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
164167
val event = params.event
165168
val highlight = params.isHighlighted
@@ -480,6 +483,7 @@ class MessageItemFactory @Inject constructor(
480483
highlight,
481484
callback,
482485
attributes,
486+
useRichTextEditorStyle = vectorPreferences.isRichTextEditorEnabled(),
483487
)
484488
}
485489

@@ -586,7 +590,7 @@ class MessageItemFactory @Inject constructor(
586590
val replyToContent = messageContent.relatesTo?.inReplyTo
587591
buildFormattedTextItem(matrixFormattedBody, informationData, highlight, callback, attributes, replyToContent)
588592
} else {
589-
buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes)
593+
buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes, useRichTextEditorStyle)
590594
}
591595
}
592596

@@ -610,6 +614,7 @@ class MessageItemFactory @Inject constructor(
610614
highlight,
611615
callback,
612616
attributes,
617+
useRichTextEditorStyle,
613618
)
614619
}
615620

@@ -620,6 +625,7 @@ class MessageItemFactory @Inject constructor(
620625
highlight: Boolean,
621626
callback: TimelineEventController.Callback?,
622627
attributes: AbsMessageItem.Attributes,
628+
useRichTextEditorStyle: Boolean,
623629
): MessageTextItem? {
624630
val renderedBody = textRenderer.render(body)
625631
val bindingOptions = spanUtils.getBindingOptions(renderedBody)
@@ -640,6 +646,7 @@ class MessageItemFactory @Inject constructor(
640646
.previewUrlRetriever(callback?.getPreviewUrlRetriever())
641647
.imageContentRenderer(imageContentRenderer)
642648
.previewUrlCallback(callback)
649+
.useRichTextEditorStyle(useRichTextEditorStyle)
643650
.leftGuideline(avatarSizeProvider.leftGuideline)
644651
.attributes(attributes)
645652
.highlighted(highlight)

vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail.timeline.item
1818

1919
import android.text.Spanned
2020
import android.text.method.MovementMethod
21+
import android.view.ViewStub
2122
import androidx.appcompat.widget.AppCompatTextView
2223
import androidx.core.text.PrecomputedTextCompat
2324
import androidx.core.view.isVisible
@@ -67,6 +68,9 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
6768
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
6869
var markwonPlugins: (List<MarkwonPlugin>)? = null
6970

71+
@EpoxyAttribute
72+
var useRichTextEditorStyle: Boolean = false
73+
7074
private val previewUrlViewUpdater = PreviewUrlViewUpdater()
7175

7276
override fun bind(holder: Holder) {
@@ -82,27 +86,28 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
8286
holder.previewUrlView.delegate = previewUrlCallback
8387
holder.previewUrlView.renderMessageLayout(attributes.informationData.messageLayout)
8488

89+
val messageView: AppCompatTextView = if (useRichTextEditorStyle) holder.richMessageView else holder.plainMessageView
8590
if (useBigFont) {
86-
holder.messageView.textSize = 44F
91+
messageView.textSize = 44F
8792
} else {
88-
holder.messageView.textSize = 15.5F
93+
messageView.textSize = 15.5F
8994
}
9095
if (searchForPills) {
9196
message?.charSequence?.findPillsAndProcess(coroutineScope) {
9297
// mmm.. not sure this is so safe in regards to cell reuse
93-
it.bind(holder.messageView)
98+
it.bind(messageView)
9499
}
95100
}
96101
message?.charSequence.let { charSequence ->
97-
markwonPlugins?.forEach { plugin -> plugin.beforeSetText(holder.messageView, charSequence as Spanned) }
102+
markwonPlugins?.forEach { plugin -> plugin.beforeSetText(messageView, charSequence as Spanned) }
98103
}
99104
super.bind(holder)
100-
holder.messageView.movementMethod = movementMethod
101-
renderSendState(holder.messageView, holder.messageView)
102-
holder.messageView.onClick(attributes.itemClickListener)
103-
holder.messageView.onLongClickIgnoringLinks(attributes.itemLongClickListener)
104-
holder.messageView.setTextWithEmojiSupport(message?.charSequence, bindingOptions)
105-
markwonPlugins?.forEach { plugin -> plugin.afterSetText(holder.messageView) }
105+
messageView.movementMethod = movementMethod
106+
renderSendState(messageView, messageView)
107+
messageView.onClick(attributes.itemClickListener)
108+
messageView.onLongClickIgnoringLinks(attributes.itemLongClickListener)
109+
messageView.setTextWithEmojiSupport(message?.charSequence, bindingOptions)
110+
markwonPlugins?.forEach { plugin -> plugin.afterSetText(messageView) }
106111
}
107112

108113
private fun AppCompatTextView.setTextWithEmojiSupport(message: CharSequence?, bindingOptions: BindingOptions?) {
@@ -125,8 +130,15 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
125130
override fun getViewStubId() = STUB_ID
126131

127132
class Holder : AbsMessageItem.Holder(STUB_ID) {
128-
val messageView by bind<AppCompatTextView>(R.id.messageTextView)
129133
val previewUrlView by bind<PreviewUrlView>(R.id.messageUrlPreview)
134+
private val richMessageStub by bind<ViewStub>(R.id.richMessageTextViewStub)
135+
private val plainMessageStub by bind<ViewStub>(R.id.plainMessageTextViewStub)
136+
val richMessageView: AppCompatTextView by lazy {
137+
richMessageStub.inflate().findViewById(R.id.messageTextView)
138+
}
139+
val plainMessageView: AppCompatTextView by lazy {
140+
plainMessageStub.inflate().findViewById(R.id.messageTextView)
141+
}
130142
}
131143

132144
inner class PreviewUrlViewUpdater : PreviewUrlRetriever.PreviewUrlRetrieverListener {

0 commit comments

Comments
 (0)