mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 02:15:35 +03:00
Timeline: Start handling code blocks. [WIP]
This commit is contained in:
parent
118870bc41
commit
3517873156
10 changed files with 328 additions and 179 deletions
|
@ -219,7 +219,7 @@ dependencies {
|
|||
def epoxy_version = '3.8.0'
|
||||
def arrow_version = "0.8.2"
|
||||
def coroutines_version = "1.3.2"
|
||||
def markwon_version = '3.1.0'
|
||||
def markwon_version = '4.1.2'
|
||||
def big_image_viewer_version = '1.5.6'
|
||||
def glide_version = '4.10.0'
|
||||
def moshi_version = '1.8.0'
|
||||
|
@ -283,8 +283,8 @@ dependencies {
|
|||
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
||||
implementation 'com.google.android.material:material:1.1.0-beta01'
|
||||
implementation 'me.gujun.android:span:1.7'
|
||||
implementation "ru.noties.markwon:core:$markwon_version"
|
||||
implementation "ru.noties.markwon:html:$markwon_version"
|
||||
implementation "io.noties.markwon:core:$markwon_version"
|
||||
implementation "io.noties.markwon:html:$markwon_version"
|
||||
implementation 'me.saket:better-link-movement-method:2.2.0'
|
||||
implementation 'com.google.android:flexbox:1.1.1'
|
||||
|
||||
|
|
|
@ -46,9 +46,11 @@ import im.vector.riotx.features.home.room.detail.timeline.TimelineEventControlle
|
|||
import im.vector.riotx.features.home.room.detail.timeline.helper.*
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.*
|
||||
import im.vector.riotx.features.html.EventHtmlRenderer
|
||||
import im.vector.riotx.features.html.CodeVisitor
|
||||
import im.vector.riotx.features.media.ImageContentRenderer
|
||||
import im.vector.riotx.features.media.VideoContentRenderer
|
||||
import me.gujun.android.span.span
|
||||
import org.commonmark.node.Document
|
||||
import javax.inject.Inject
|
||||
|
||||
class MessageItemFactory @Inject constructor(
|
||||
|
@ -234,22 +236,33 @@ class MessageItemFactory @Inject constructor(
|
|||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?,
|
||||
attributes: AbsMessageItem.Attributes): MessageTextItem? {
|
||||
|
||||
val isFormatted = messageContent.formattedBody.isNullOrBlank().not()
|
||||
val bodyToUse = messageContent.formattedBody?.let {
|
||||
htmlRenderer.get().render(it.trim())
|
||||
} ?: messageContent.body
|
||||
val bodyToUse = if (isFormatted) {
|
||||
val formattedBody = htmlRenderer.get().parse(messageContent.body) as Document
|
||||
val codeVisitor = CodeVisitor()
|
||||
codeVisitor.visit(formattedBody)
|
||||
if (codeVisitor.codeKind == CodeVisitor.Kind.NONE) {
|
||||
messageContent.formattedBody.let {
|
||||
htmlRenderer.get().render(it!!.trim())
|
||||
}
|
||||
} else {
|
||||
htmlRenderer.get().render(formattedBody)
|
||||
}
|
||||
} else {
|
||||
messageContent.body
|
||||
}
|
||||
|
||||
val linkifiedBody = linkifyBody(bodyToUse, callback)
|
||||
|
||||
return MessageTextItem_()
|
||||
.apply {
|
||||
if (informationData.hasBeenEdited) {
|
||||
val spannable = annotateWithEdited(linkifiedBody, callback, informationData)
|
||||
message(spannable)
|
||||
} else {
|
||||
message(linkifiedBody)
|
||||
}
|
||||
}
|
||||
return MessageTextItem_().apply {
|
||||
if (informationData.hasBeenEdited) {
|
||||
val spannable = annotateWithEdited(linkifiedBody, callback, informationData)
|
||||
message(spannable)
|
||||
} else {
|
||||
message(linkifiedBody)
|
||||
}
|
||||
}
|
||||
.useBigFont(linkifiedBody.length <= MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT * 2 && containsOnlyEmojis(linkifiedBody.toString()))
|
||||
.searchForPills(isFormatted)
|
||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright 2019 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.riotx.features.home.room.detail.timeline.item
|
||||
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||
abstract class MessageBlockCodeItem : AbsMessageItem<MessageBlockCodeItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute
|
||||
var message: CharSequence? = null
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
|
||||
}
|
||||
|
||||
override fun getViewType() = STUB_ID
|
||||
|
||||
class Holder : AbsMessageItem.Holder(STUB_ID) {
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val STUB_ID = R.id.messageContentCodeBlockStub
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright 2019 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.riotx.features.html
|
||||
|
||||
import org.commonmark.node.AbstractVisitor
|
||||
import org.commonmark.node.Code
|
||||
import org.commonmark.node.FencedCodeBlock
|
||||
import org.commonmark.node.IndentedCodeBlock
|
||||
|
||||
/**
|
||||
* This class is in charge of visiting nodes and tells if we have some code nodes (inline or block).
|
||||
*/
|
||||
class CodeVisitor : AbstractVisitor() {
|
||||
|
||||
var codeKind: Kind = Kind.NONE
|
||||
private set
|
||||
|
||||
override fun visit(fencedCodeBlock: FencedCodeBlock?) {
|
||||
if (codeKind == Kind.NONE) {
|
||||
codeKind = Kind.BLOCK
|
||||
}
|
||||
}
|
||||
|
||||
override fun visit(indentedCodeBlock: IndentedCodeBlock?) {
|
||||
if (codeKind == Kind.NONE) {
|
||||
codeKind = Kind.BLOCK
|
||||
}
|
||||
}
|
||||
|
||||
override fun visit(code: Code?) {
|
||||
if (codeKind == Kind.NONE) {
|
||||
codeKind = Kind.INLINE
|
||||
}
|
||||
}
|
||||
|
||||
enum class Kind {
|
||||
NONE,
|
||||
INLINE,
|
||||
BLOCK
|
||||
}
|
||||
|
||||
}
|
|
@ -17,171 +17,47 @@
|
|||
package im.vector.riotx.features.html
|
||||
|
||||
import android.content.Context
|
||||
import android.text.style.URLSpan
|
||||
import im.vector.matrix.android.api.permalinks.PermalinkData
|
||||
import im.vector.matrix.android.api.permalinks.PermalinkParser
|
||||
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||
import im.vector.riotx.core.glide.GlideApp
|
||||
import im.vector.riotx.core.glide.GlideRequests
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
import org.commonmark.node.BlockQuote
|
||||
import org.commonmark.node.HtmlBlock
|
||||
import org.commonmark.node.HtmlInline
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.html.HtmlPlugin
|
||||
import io.noties.markwon.html.TagHandlerNoOp
|
||||
import org.commonmark.node.Node
|
||||
import ru.noties.markwon.*
|
||||
import ru.noties.markwon.html.HtmlTag
|
||||
import ru.noties.markwon.html.MarkwonHtmlParserImpl
|
||||
import ru.noties.markwon.html.MarkwonHtmlRenderer
|
||||
import ru.noties.markwon.html.TagHandler
|
||||
import ru.noties.markwon.html.tag.*
|
||||
import java.util.Arrays.asList
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class EventHtmlRenderer @Inject constructor(context: Context,
|
||||
avatarRenderer: AvatarRenderer,
|
||||
sessionHolder: ActiveSessionHolder) {
|
||||
htmlConfigure: MatrixHtmlPluginConfigure) {
|
||||
|
||||
private val markwon = Markwon.builder(context)
|
||||
.usePlugin(MatrixPlugin.create(GlideApp.with(context), context, avatarRenderer, sessionHolder))
|
||||
.usePlugin(HtmlPlugin.create(htmlConfigure))
|
||||
.build()
|
||||
|
||||
fun parse(text: String): Node {
|
||||
return markwon.parse(text)
|
||||
}
|
||||
|
||||
fun render(text: String): CharSequence {
|
||||
return markwon.toMarkdown(text)
|
||||
}
|
||||
|
||||
fun render(node: Node) : CharSequence {
|
||||
fun render(node: Node): CharSequence {
|
||||
return markwon.render(node)
|
||||
}
|
||||
}
|
||||
|
||||
private class MatrixPlugin private constructor(private val glideRequests: GlideRequests,
|
||||
private val context: Context,
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val session: ActiveSessionHolder) : AbstractMarkwonPlugin() {
|
||||
class MatrixHtmlPluginConfigure @Inject constructor(private val context: Context,
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val session: ActiveSessionHolder) : HtmlPlugin.HtmlConfigure {
|
||||
|
||||
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
|
||||
builder.htmlParser(MarkwonHtmlParserImpl.create())
|
||||
override fun configureHtml(plugin: HtmlPlugin) {
|
||||
plugin
|
||||
.addHandler(TagHandlerNoOp.create("a"))
|
||||
.addHandler(FontTagHandler())
|
||||
.addHandler(MxLinkTagHandler(GlideApp.with(context), context, avatarRenderer, session))
|
||||
.addHandler(MxReplyTagHandler())
|
||||
}
|
||||
|
||||
override fun configureHtmlRenderer(builder: MarkwonHtmlRenderer.Builder) {
|
||||
builder
|
||||
.setHandler(
|
||||
"img",
|
||||
ImageHandler.create())
|
||||
.setHandler(
|
||||
"a",
|
||||
MxLinkHandler(glideRequests, context, avatarRenderer, session))
|
||||
.setHandler(
|
||||
"blockquote",
|
||||
BlockquoteHandler())
|
||||
.setHandler(
|
||||
"font",
|
||||
FontTagHandler())
|
||||
.setHandler(
|
||||
"sub",
|
||||
SubScriptHandler())
|
||||
.setHandler(
|
||||
"sup",
|
||||
SuperScriptHandler())
|
||||
.setHandler(
|
||||
asList<String>("b", "strong"),
|
||||
StrongEmphasisHandler())
|
||||
.setHandler(
|
||||
asList<String>("s", "del"),
|
||||
StrikeHandler())
|
||||
.setHandler(
|
||||
asList<String>("u", "ins"),
|
||||
UnderlineHandler())
|
||||
.setHandler(
|
||||
asList<String>("ul", "ol"),
|
||||
ListHandler())
|
||||
.setHandler(
|
||||
asList<String>("i", "em", "cite", "dfn"),
|
||||
EmphasisHandler())
|
||||
.setHandler(
|
||||
asList<String>("h1", "h2", "h3", "h4", "h5", "h6"),
|
||||
HeadingHandler())
|
||||
.setHandler("mx-reply",
|
||||
MxReplyTagHandler())
|
||||
}
|
||||
|
||||
override fun afterRender(node: Node, visitor: MarkwonVisitor) {
|
||||
val configuration = visitor.configuration()
|
||||
configuration.htmlRenderer().render(visitor, configuration.htmlParser())
|
||||
}
|
||||
|
||||
override fun configureVisitor(builder: MarkwonVisitor.Builder) {
|
||||
builder
|
||||
.on(HtmlBlock::class.java) { visitor, htmlBlock -> visitHtml(visitor, htmlBlock.literal) }
|
||||
.on(HtmlInline::class.java) { visitor, htmlInline -> visitHtml(visitor, htmlInline.literal) }
|
||||
}
|
||||
|
||||
private fun visitHtml(visitor: MarkwonVisitor, html: String?) {
|
||||
if (html != null) {
|
||||
visitor.configuration().htmlParser().processFragment(visitor.builder(), html)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun create(glideRequests: GlideRequests, context: Context, avatarRenderer: AvatarRenderer, session: ActiveSessionHolder): MatrixPlugin {
|
||||
return MatrixPlugin(glideRequests, context, avatarRenderer, session)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class MxLinkHandler(private val glideRequests: GlideRequests,
|
||||
private val context: Context,
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val sessionHolder: ActiveSessionHolder) : TagHandler() {
|
||||
|
||||
private val linkHandler = LinkHandler()
|
||||
|
||||
override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) {
|
||||
val link = tag.attributes()["href"]
|
||||
if (link != null) {
|
||||
val permalinkData = PermalinkParser.parse(link)
|
||||
when (permalinkData) {
|
||||
is PermalinkData.UserLink -> {
|
||||
val user = sessionHolder.getSafeActiveSession()?.getUser(permalinkData.userId)
|
||||
val span = PillImageSpan(glideRequests, avatarRenderer, context, permalinkData.userId, user)
|
||||
SpannableBuilder.setSpans(
|
||||
visitor.builder(),
|
||||
span,
|
||||
tag.start(),
|
||||
tag.end()
|
||||
)
|
||||
// also add clickable span
|
||||
SpannableBuilder.setSpans(
|
||||
visitor.builder(),
|
||||
URLSpan(link),
|
||||
tag.start(),
|
||||
tag.end()
|
||||
)
|
||||
}
|
||||
else -> linkHandler.handle(visitor, renderer, tag)
|
||||
}
|
||||
} else {
|
||||
linkHandler.handle(visitor, renderer, tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class MxReplyTagHandler : TagHandler() {
|
||||
|
||||
override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) {
|
||||
val configuration = visitor.configuration()
|
||||
val factory = configuration.spansFactory().get(BlockQuote::class.java)
|
||||
if (factory != null) {
|
||||
SpannableBuilder.setSpans(
|
||||
visitor.builder(),
|
||||
factory.getSpans(configuration, visitor.renderProps()),
|
||||
tag.start(),
|
||||
tag.end()
|
||||
)
|
||||
val replyText = visitor.builder().removeFromEnd(tag.end())
|
||||
visitor.builder().append("\n\n").append(replyText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,15 +17,18 @@ package im.vector.riotx.features.html
|
|||
|
||||
import android.graphics.Color
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import ru.noties.markwon.MarkwonConfiguration
|
||||
import ru.noties.markwon.RenderProps
|
||||
import ru.noties.markwon.html.HtmlTag
|
||||
import ru.noties.markwon.html.tag.SimpleTagHandler
|
||||
import io.noties.markwon.MarkwonConfiguration
|
||||
import io.noties.markwon.RenderProps
|
||||
import io.noties.markwon.html.HtmlTag
|
||||
import io.noties.markwon.html.tag.SimpleTagHandler
|
||||
|
||||
/**
|
||||
* custom to matrix for IRC-style font coloring
|
||||
*/
|
||||
class FontTagHandler : SimpleTagHandler() {
|
||||
|
||||
override fun supportedTags() = listOf("font")
|
||||
|
||||
override fun getSpans(configuration: MarkwonConfiguration, renderProps: RenderProps, tag: HtmlTag): Any? {
|
||||
val colorString = tag.attributes()["color"]?.let { parseColor(it) } ?: Color.BLACK
|
||||
return ForegroundColorSpan(colorString)
|
||||
|
@ -37,23 +40,23 @@ class FontTagHandler : SimpleTagHandler() {
|
|||
} catch (e: Exception) {
|
||||
// try other w3c colors?
|
||||
return when (color_name) {
|
||||
"white" -> Color.WHITE
|
||||
"yellow" -> Color.YELLOW
|
||||
"white" -> Color.WHITE
|
||||
"yellow" -> Color.YELLOW
|
||||
"fuchsia" -> Color.parseColor("#FF00FF")
|
||||
"red" -> Color.RED
|
||||
"silver" -> Color.parseColor("#C0C0C0")
|
||||
"gray" -> Color.GRAY
|
||||
"olive" -> Color.parseColor("#808000")
|
||||
"purple" -> Color.parseColor("#800080")
|
||||
"maroon" -> Color.parseColor("#800000")
|
||||
"aqua" -> Color.parseColor("#00FFFF")
|
||||
"lime" -> Color.parseColor("#00FF00")
|
||||
"teal" -> Color.parseColor("#008080")
|
||||
"green" -> Color.GREEN
|
||||
"blue" -> Color.BLUE
|
||||
"orange" -> Color.parseColor("#FFA500")
|
||||
"navy" -> Color.parseColor("#000080")
|
||||
else -> Color.BLACK
|
||||
"red" -> Color.RED
|
||||
"silver" -> Color.parseColor("#C0C0C0")
|
||||
"gray" -> Color.GRAY
|
||||
"olive" -> Color.parseColor("#808000")
|
||||
"purple" -> Color.parseColor("#800080")
|
||||
"maroon" -> Color.parseColor("#800000")
|
||||
"aqua" -> Color.parseColor("#00FFFF")
|
||||
"lime" -> Color.parseColor("#00FF00")
|
||||
"teal" -> Color.parseColor("#008080")
|
||||
"green" -> Color.GREEN
|
||||
"blue" -> Color.BLUE
|
||||
"orange" -> Color.parseColor("#FFA500")
|
||||
"navy" -> Color.parseColor("#000080")
|
||||
else -> Color.BLACK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright 2019 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.riotx.features.html
|
||||
|
||||
import android.content.Context
|
||||
import android.text.style.URLSpan
|
||||
import im.vector.matrix.android.api.permalinks.PermalinkData
|
||||
import im.vector.matrix.android.api.permalinks.PermalinkParser
|
||||
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||
import im.vector.riotx.core.glide.GlideRequests
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
import io.noties.markwon.MarkwonVisitor
|
||||
import io.noties.markwon.SpannableBuilder
|
||||
import io.noties.markwon.html.HtmlTag
|
||||
import io.noties.markwon.html.MarkwonHtmlRenderer
|
||||
import io.noties.markwon.html.tag.LinkHandler
|
||||
|
||||
class MxLinkTagHandler(private val glideRequests: GlideRequests,
|
||||
private val context: Context,
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val sessionHolder: ActiveSessionHolder) : LinkHandler() {
|
||||
|
||||
override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) {
|
||||
val link = tag.attributes()["href"]
|
||||
if (link != null) {
|
||||
val permalinkData = PermalinkParser.parse(link)
|
||||
when (permalinkData) {
|
||||
is PermalinkData.UserLink -> {
|
||||
val user = sessionHolder.getSafeActiveSession()?.getUser(permalinkData.userId)
|
||||
val span = PillImageSpan(glideRequests, avatarRenderer, context, permalinkData.userId, user)
|
||||
SpannableBuilder.setSpans(
|
||||
visitor.builder(),
|
||||
span,
|
||||
tag.start(),
|
||||
tag.end()
|
||||
)
|
||||
// also add clickable span
|
||||
SpannableBuilder.setSpans(
|
||||
visitor.builder(),
|
||||
URLSpan(link),
|
||||
tag.start(),
|
||||
tag.end()
|
||||
)
|
||||
}
|
||||
else -> super.handle(visitor, renderer, tag)
|
||||
}
|
||||
} else {
|
||||
super.handle(visitor, renderer, tag)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright 2019 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.riotx.features.html
|
||||
|
||||
import io.noties.markwon.MarkwonVisitor
|
||||
import io.noties.markwon.SpannableBuilder
|
||||
import io.noties.markwon.html.HtmlTag
|
||||
import io.noties.markwon.html.MarkwonHtmlRenderer
|
||||
import io.noties.markwon.html.TagHandler
|
||||
import org.commonmark.node.BlockQuote
|
||||
|
||||
class MxReplyTagHandler : TagHandler() {
|
||||
|
||||
override fun supportedTags() = listOf("mx-reply")
|
||||
|
||||
override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) {
|
||||
val configuration = visitor.configuration()
|
||||
val factory = configuration.spansFactory().get(BlockQuote::class.java)
|
||||
if (factory != null) {
|
||||
SpannableBuilder.setSpans(
|
||||
visitor.builder(),
|
||||
factory.getSpans(configuration, visitor.renderProps()),
|
||||
tag.start(),
|
||||
tag.end()
|
||||
)
|
||||
val replyText = visitor.builder().removeFromEnd(tag.end())
|
||||
visitor.builder().append("\n\n").append(replyText)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -78,6 +78,14 @@
|
|||
android:layout="@layout/item_timeline_event_text_message_stub"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/messageContentCodeBlockStub"
|
||||
style="@style/TimelineContentStubBaseParams"
|
||||
android:layout_height="wrap_content"
|
||||
android:inflatedId="@id/messageTextView"
|
||||
android:layout="@layout/item_timeline_event_code_block_stub"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/messageContentMediaStub"
|
||||
style="@style/TimelineContentStubBaseParams"
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/messagesAdapter_body_hsv"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxHeight="500dp">
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/codeBlockScrollView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:fadeScrollbars="false"
|
||||
android:scrollbars="vertical|horizontal"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<HorizontalScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/codeBlockTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginLeft="4dp"
|
||||
android:autoLink="none"
|
||||
android:fontFamily="monospace"
|
||||
android:textSize="14sp"
|
||||
tools:text="" />
|
||||
|
||||
</HorizontalScrollView>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
Loading…
Reference in a new issue