mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 10:25: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 epoxy_version = '3.8.0'
|
||||||
def arrow_version = "0.8.2"
|
def arrow_version = "0.8.2"
|
||||||
def coroutines_version = "1.3.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 big_image_viewer_version = '1.5.6'
|
||||||
def glide_version = '4.10.0'
|
def glide_version = '4.10.0'
|
||||||
def moshi_version = '1.8.0'
|
def moshi_version = '1.8.0'
|
||||||
|
@ -283,8 +283,8 @@ dependencies {
|
||||||
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
||||||
implementation 'com.google.android.material:material:1.1.0-beta01'
|
implementation 'com.google.android.material:material:1.1.0-beta01'
|
||||||
implementation 'me.gujun.android:span:1.7'
|
implementation 'me.gujun.android:span:1.7'
|
||||||
implementation "ru.noties.markwon:core:$markwon_version"
|
implementation "io.noties.markwon:core:$markwon_version"
|
||||||
implementation "ru.noties.markwon:html:$markwon_version"
|
implementation "io.noties.markwon:html:$markwon_version"
|
||||||
implementation 'me.saket:better-link-movement-method:2.2.0'
|
implementation 'me.saket:better-link-movement-method:2.2.0'
|
||||||
implementation 'com.google.android:flexbox:1.1.1'
|
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.helper.*
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.*
|
import im.vector.riotx.features.home.room.detail.timeline.item.*
|
||||||
import im.vector.riotx.features.html.EventHtmlRenderer
|
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.ImageContentRenderer
|
||||||
import im.vector.riotx.features.media.VideoContentRenderer
|
import im.vector.riotx.features.media.VideoContentRenderer
|
||||||
import me.gujun.android.span.span
|
import me.gujun.android.span.span
|
||||||
|
import org.commonmark.node.Document
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class MessageItemFactory @Inject constructor(
|
class MessageItemFactory @Inject constructor(
|
||||||
|
@ -234,22 +236,33 @@ class MessageItemFactory @Inject constructor(
|
||||||
highlight: Boolean,
|
highlight: Boolean,
|
||||||
callback: TimelineEventController.Callback?,
|
callback: TimelineEventController.Callback?,
|
||||||
attributes: AbsMessageItem.Attributes): MessageTextItem? {
|
attributes: AbsMessageItem.Attributes): MessageTextItem? {
|
||||||
|
|
||||||
val isFormatted = messageContent.formattedBody.isNullOrBlank().not()
|
val isFormatted = messageContent.formattedBody.isNullOrBlank().not()
|
||||||
val bodyToUse = messageContent.formattedBody?.let {
|
val bodyToUse = if (isFormatted) {
|
||||||
htmlRenderer.get().render(it.trim())
|
val formattedBody = htmlRenderer.get().parse(messageContent.body) as Document
|
||||||
} ?: messageContent.body
|
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)
|
val linkifiedBody = linkifyBody(bodyToUse, callback)
|
||||||
|
|
||||||
return MessageTextItem_()
|
return MessageTextItem_().apply {
|
||||||
.apply {
|
if (informationData.hasBeenEdited) {
|
||||||
if (informationData.hasBeenEdited) {
|
val spannable = annotateWithEdited(linkifiedBody, callback, informationData)
|
||||||
val spannable = annotateWithEdited(linkifiedBody, callback, informationData)
|
message(spannable)
|
||||||
message(spannable)
|
} else {
|
||||||
} else {
|
message(linkifiedBody)
|
||||||
message(linkifiedBody)
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.useBigFont(linkifiedBody.length <= MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT * 2 && containsOnlyEmojis(linkifiedBody.toString()))
|
.useBigFont(linkifiedBody.length <= MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT * 2 && containsOnlyEmojis(linkifiedBody.toString()))
|
||||||
.searchForPills(isFormatted)
|
.searchForPills(isFormatted)
|
||||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
.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
|
package im.vector.riotx.features.html
|
||||||
|
|
||||||
import android.content.Context
|
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.di.ActiveSessionHolder
|
||||||
import im.vector.riotx.core.glide.GlideApp
|
import im.vector.riotx.core.glide.GlideApp
|
||||||
import im.vector.riotx.core.glide.GlideRequests
|
|
||||||
import im.vector.riotx.features.home.AvatarRenderer
|
import im.vector.riotx.features.home.AvatarRenderer
|
||||||
import org.commonmark.node.BlockQuote
|
import io.noties.markwon.Markwon
|
||||||
import org.commonmark.node.HtmlBlock
|
import io.noties.markwon.html.HtmlPlugin
|
||||||
import org.commonmark.node.HtmlInline
|
import io.noties.markwon.html.TagHandlerNoOp
|
||||||
import org.commonmark.node.Node
|
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.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class EventHtmlRenderer @Inject constructor(context: Context,
|
class EventHtmlRenderer @Inject constructor(context: Context,
|
||||||
avatarRenderer: AvatarRenderer,
|
htmlConfigure: MatrixHtmlPluginConfigure) {
|
||||||
sessionHolder: ActiveSessionHolder) {
|
|
||||||
private val markwon = Markwon.builder(context)
|
private val markwon = Markwon.builder(context)
|
||||||
.usePlugin(MatrixPlugin.create(GlideApp.with(context), context, avatarRenderer, sessionHolder))
|
.usePlugin(HtmlPlugin.create(htmlConfigure))
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
fun parse(text: String): Node {
|
||||||
|
return markwon.parse(text)
|
||||||
|
}
|
||||||
|
|
||||||
fun render(text: String): CharSequence {
|
fun render(text: String): CharSequence {
|
||||||
return markwon.toMarkdown(text)
|
return markwon.toMarkdown(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun render(node: Node) : CharSequence {
|
fun render(node: Node): CharSequence {
|
||||||
return markwon.render(node)
|
return markwon.render(node)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class MatrixPlugin private constructor(private val glideRequests: GlideRequests,
|
class MatrixHtmlPluginConfigure @Inject constructor(private val context: Context,
|
||||||
private val context: Context,
|
private val avatarRenderer: AvatarRenderer,
|
||||||
private val avatarRenderer: AvatarRenderer,
|
private val session: ActiveSessionHolder) : HtmlPlugin.HtmlConfigure {
|
||||||
private val session: ActiveSessionHolder) : AbstractMarkwonPlugin() {
|
|
||||||
|
|
||||||
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
|
override fun configureHtml(plugin: HtmlPlugin) {
|
||||||
builder.htmlParser(MarkwonHtmlParserImpl.create())
|
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.graphics.Color
|
||||||
import android.text.style.ForegroundColorSpan
|
import android.text.style.ForegroundColorSpan
|
||||||
import ru.noties.markwon.MarkwonConfiguration
|
import io.noties.markwon.MarkwonConfiguration
|
||||||
import ru.noties.markwon.RenderProps
|
import io.noties.markwon.RenderProps
|
||||||
import ru.noties.markwon.html.HtmlTag
|
import io.noties.markwon.html.HtmlTag
|
||||||
import ru.noties.markwon.html.tag.SimpleTagHandler
|
import io.noties.markwon.html.tag.SimpleTagHandler
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* custom to matrix for IRC-style font coloring
|
* custom to matrix for IRC-style font coloring
|
||||||
*/
|
*/
|
||||||
class FontTagHandler : SimpleTagHandler() {
|
class FontTagHandler : SimpleTagHandler() {
|
||||||
|
|
||||||
|
override fun supportedTags() = listOf("font")
|
||||||
|
|
||||||
override fun getSpans(configuration: MarkwonConfiguration, renderProps: RenderProps, tag: HtmlTag): Any? {
|
override fun getSpans(configuration: MarkwonConfiguration, renderProps: RenderProps, tag: HtmlTag): Any? {
|
||||||
val colorString = tag.attributes()["color"]?.let { parseColor(it) } ?: Color.BLACK
|
val colorString = tag.attributes()["color"]?.let { parseColor(it) } ?: Color.BLACK
|
||||||
return ForegroundColorSpan(colorString)
|
return ForegroundColorSpan(colorString)
|
||||||
|
@ -37,23 +40,23 @@ class FontTagHandler : SimpleTagHandler() {
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// try other w3c colors?
|
// try other w3c colors?
|
||||||
return when (color_name) {
|
return when (color_name) {
|
||||||
"white" -> Color.WHITE
|
"white" -> Color.WHITE
|
||||||
"yellow" -> Color.YELLOW
|
"yellow" -> Color.YELLOW
|
||||||
"fuchsia" -> Color.parseColor("#FF00FF")
|
"fuchsia" -> Color.parseColor("#FF00FF")
|
||||||
"red" -> Color.RED
|
"red" -> Color.RED
|
||||||
"silver" -> Color.parseColor("#C0C0C0")
|
"silver" -> Color.parseColor("#C0C0C0")
|
||||||
"gray" -> Color.GRAY
|
"gray" -> Color.GRAY
|
||||||
"olive" -> Color.parseColor("#808000")
|
"olive" -> Color.parseColor("#808000")
|
||||||
"purple" -> Color.parseColor("#800080")
|
"purple" -> Color.parseColor("#800080")
|
||||||
"maroon" -> Color.parseColor("#800000")
|
"maroon" -> Color.parseColor("#800000")
|
||||||
"aqua" -> Color.parseColor("#00FFFF")
|
"aqua" -> Color.parseColor("#00FFFF")
|
||||||
"lime" -> Color.parseColor("#00FF00")
|
"lime" -> Color.parseColor("#00FF00")
|
||||||
"teal" -> Color.parseColor("#008080")
|
"teal" -> Color.parseColor("#008080")
|
||||||
"green" -> Color.GREEN
|
"green" -> Color.GREEN
|
||||||
"blue" -> Color.BLUE
|
"blue" -> Color.BLUE
|
||||||
"orange" -> Color.parseColor("#FFA500")
|
"orange" -> Color.parseColor("#FFA500")
|
||||||
"navy" -> Color.parseColor("#000080")
|
"navy" -> Color.parseColor("#000080")
|
||||||
else -> Color.BLACK
|
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"
|
android:layout="@layout/item_timeline_event_text_message_stub"
|
||||||
tools:visibility="visible" />
|
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
|
<ViewStub
|
||||||
android:id="@+id/messageContentMediaStub"
|
android:id="@+id/messageContentMediaStub"
|
||||||
style="@style/TimelineContentStubBaseParams"
|
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