Merge pull request #654 from vector-im/feature/timeline_message_code

Feature/timeline message code
This commit is contained in:
Benoit Marty 2019-10-31 15:08:13 +01:00 committed by GitHub
commit 36060fe332
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 367 additions and 225 deletions

View File

@ -5,7 +5,7 @@ Features ✨:
- -
Improvements 🙌: Improvements 🙌:
- - Handle code tags (#567)
Other changes: Other changes:
- Accessibility improvements to the attachment file type chooser - Accessibility improvements to the attachment file type chooser

View File

@ -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'

View File

@ -16,17 +16,15 @@
package im.vector.riotx.core.platform package im.vector.riotx.core.platform
import android.annotation.TargetApi
import android.content.Context import android.content.Context
import android.os.Build
import android.util.AttributeSet import android.util.AttributeSet
import android.widget.ScrollView import androidx.core.widget.NestedScrollView
import im.vector.riotx.R import im.vector.riotx.R
private const val DEFAULT_MAX_HEIGHT = 200 private const val DEFAULT_MAX_HEIGHT = 200
class MaxHeightScrollView : ScrollView { class MaxHeightScrollView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0)
: NestedScrollView(context, attrs, defStyle) {
var maxHeight: Int = 0 var maxHeight: Int = 0
set(value) { set(value) {
@ -34,28 +32,7 @@ class MaxHeightScrollView : ScrollView {
requestLayout() requestLayout()
} }
constructor(context: Context) : super(context) {} init {
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
if (!isInEditMode) {
init(context, attrs)
}
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
if (!isInEditMode) {
init(context, attrs)
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
if (!isInEditMode) {
init(context, attrs)
}
}
private fun init(context: Context, attrs: AttributeSet?) {
if (attrs != null) { if (attrs != null) {
val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.MaxHeightScrollView) val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.MaxHeightScrollView)
maxHeight = styledAttrs.getDimensionPixelSize(R.styleable.MaxHeightScrollView_maxHeight, DEFAULT_MAX_HEIGHT) maxHeight = styledAttrs.getDimensionPixelSize(R.styleable.MaxHeightScrollView_maxHeight, DEFAULT_MAX_HEIGHT)

View File

@ -480,7 +480,7 @@ class RoomDetailFragment :
jumpToReadMarkerView.render(show, readMarkerId) jumpToReadMarkerView.render(show, readMarkerId)
} }
} }
recyclerView.setController(timelineEventController) recyclerView.adapter = timelineEventController.adapter
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE) { if (recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE) {

View File

@ -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(
@ -97,16 +99,8 @@ class MessageItemFactory @Inject constructor(
// val all = event.root.toContent() // val all = event.root.toContent()
// val ev = all.toModel<Event>() // val ev = all.toModel<Event>()
return when (messageContent) { return when (messageContent) {
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes)
informationData, is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes)
highlight,
callback,
attributes)
is MessageTextContent -> buildTextMessageItem(messageContent,
informationData,
highlight,
callback,
attributes)
is MessageImageContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageImageContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes)
@ -229,34 +223,75 @@ class MessageItemFactory @Inject constructor(
.clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) } .clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) }
} }
private fun buildTextMessageItem(messageContent: MessageTextContent, private fun buildItemForTextContent(messageContent: MessageTextContent,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? {
val isFormatted = messageContent.formattedBody.isNullOrBlank().not()
return if (isFormatted) {
val localFormattedBody = htmlRenderer.get().parse(messageContent.body) as Document
val codeVisitor = CodeVisitor()
codeVisitor.visit(localFormattedBody)
when (codeVisitor.codeKind) {
CodeVisitor.Kind.BLOCK -> {
val codeFormattedBlock = htmlRenderer.get().render(localFormattedBody)
buildCodeBlockItem(codeFormattedBlock, informationData, highlight, callback, attributes)
}
CodeVisitor.Kind.INLINE -> {
val codeFormatted = htmlRenderer.get().render(localFormattedBody)
buildMessageTextItem(codeFormatted, false, informationData, highlight, callback, attributes)
}
CodeVisitor.Kind.NONE -> {
val formattedBody = htmlRenderer.get().render(messageContent.formattedBody!!)
buildMessageTextItem(formattedBody, true, informationData, highlight, callback, attributes)
}
}
} else {
buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes)
}
}
private fun buildMessageTextItem(body: CharSequence,
isFormatted: Boolean,
informationData: MessageInformationData, informationData: MessageInformationData,
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 linkifiedBody = linkifyBody(body, callback)
val bodyToUse = messageContent.formattedBody?.let {
htmlRenderer.get().render(it.trim())
} ?: messageContent.body
val linkifiedBody = linkifyBody(bodyToUse, callback) return MessageTextItem_().apply {
if (informationData.hasBeenEdited) {
return MessageTextItem_() val spannable = annotateWithEdited(linkifiedBody, callback, informationData)
.apply { message(spannable)
if (informationData.hasBeenEdited) { } else {
val spannable = annotateWithEdited(linkifiedBody, callback, informationData) message(linkifiedBody)
message(spannable) }
} else { }
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)
.attributes(attributes) .attributes(attributes)
.highlighted(highlight) .highlighted(highlight)
.urlClickCallback(callback) .urlClickCallback(callback)
// click on the text }
private fun buildCodeBlockItem(formattedBody: CharSequence,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageBlockCodeItem? {
return MessageBlockCodeItem_()
.apply {
if (informationData.hasBeenEdited) {
val spannable = annotateWithEdited("", callback, informationData)
editedSpan(spannable)
}
}
.leftGuideline(avatarSizeProvider.leftGuideline)
.attributes(attributes)
.highlighted(highlight)
.message(formattedBody)
} }
private fun annotateWithEdited(linkifiedBody: CharSequence, private fun annotateWithEdited(linkifiedBody: CharSequence,

View File

@ -0,0 +1,54 @@
/*
* 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 android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.extensions.setTextOrHide
import me.saket.bettermovementmethod.BetterLinkMovementMethod
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
abstract class MessageBlockCodeItem : AbsMessageItem<MessageBlockCodeItem.Holder>() {
@EpoxyAttribute
var message: CharSequence? = null
@EpoxyAttribute
var editedSpan: CharSequence? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.messageView.text = message
renderSendState(holder.messageView, holder.messageView)
holder.messageView.setOnClickListener(attributes.itemClickListener)
holder.messageView.setOnLongClickListener(attributes.itemLongClickListener)
holder.editedView.movementMethod = BetterLinkMovementMethod.getInstance()
holder.editedView.setTextOrHide(editedSpan)
}
override fun getViewType() = STUB_ID
class Holder : AbsMessageItem.Holder(STUB_ID) {
val messageView by bind<TextView>(R.id.codeBlockTextView)
val editedView by bind<TextView>(R.id.codeBlockEditedView)
}
companion object {
private const val STUB_ID = R.id.messageContentCodeBlockStub
}
}

View File

@ -0,0 +1,55 @@
/*
* 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
}
}

View File

@ -17,171 +17,46 @@
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())
override fun configureHtmlRenderer(builder: MarkwonHtmlRenderer.Builder) { .addHandler(MxLinkTagHandler(GlideApp.with(context), context, avatarRenderer, session))
builder .addHandler(MxReplyTagHandler())
.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)
}
} }
} }

View File

@ -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
} }
} }
} }

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -86,7 +86,7 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/roomToolbar" /> app:layout_constraintTop_toBottomOf="@id/roomToolbar" />
<com.airbnb.epoxy.EpoxyRecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView" android:id="@+id/recyclerView"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"

View File

@ -78,6 +78,13 @@
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: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"

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<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:fontFamily="monospace"
android:textSize="14sp" />
</HorizontalScrollView>
<TextView
android:id="@+id/codeBlockEditedView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp" />
</LinearLayout>