Timeline html rendering: handle code tags
This commit is contained in:
parent
8f0e1039aa
commit
a9fe21e583
|
@ -43,8 +43,6 @@ import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttrib
|
|||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker
|
||||
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageBlockCodeItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageBlockCodeItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageFileItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageFileItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageImageVideoItem
|
||||
|
@ -63,7 +61,6 @@ import im.vector.app.features.home.room.detail.timeline.item.VerificationRequest
|
|||
import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod
|
||||
import im.vector.app.features.home.room.detail.timeline.tools.linkify
|
||||
import im.vector.app.features.html.CodeVisitor
|
||||
import im.vector.app.features.html.EventHtmlRenderer
|
||||
import im.vector.app.features.html.PillsPostProcessor
|
||||
import im.vector.app.features.html.SpanUtils
|
||||
|
@ -71,7 +68,6 @@ import im.vector.app.features.html.VectorHtmlCompressor
|
|||
import im.vector.app.features.media.ImageContentRenderer
|
||||
import im.vector.app.features.media.VideoContentRenderer
|
||||
import me.gujun.android.span.span
|
||||
import org.commonmark.node.Document
|
||||
import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
|
@ -454,46 +450,22 @@ class MessageItemFactory @Inject constructor(
|
|||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?,
|
||||
attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? {
|
||||
val isFormatted = messageContent.matrixFormattedBody.isNullOrBlank().not()
|
||||
return if (isFormatted) {
|
||||
// First detect if the message contains some code block(s) or inline code
|
||||
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)
|
||||
if (codeFormattedBlock == null) {
|
||||
buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes)
|
||||
} else {
|
||||
buildCodeBlockItem(codeFormattedBlock, informationData, highlight, callback, attributes)
|
||||
}
|
||||
}
|
||||
CodeVisitor.Kind.INLINE -> {
|
||||
val codeFormatted = htmlRenderer.get().render(localFormattedBody)
|
||||
if (codeFormatted == null) {
|
||||
buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes)
|
||||
} else {
|
||||
buildMessageTextItem(codeFormatted, false, informationData, highlight, callback, attributes)
|
||||
}
|
||||
}
|
||||
CodeVisitor.Kind.NONE -> {
|
||||
buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes)
|
||||
}
|
||||
}
|
||||
val matrixFormattedBody = messageContent.matrixFormattedBody
|
||||
return if (matrixFormattedBody != null) {
|
||||
buildFormattedTextItem(matrixFormattedBody, informationData, highlight, callback, attributes)
|
||||
} else {
|
||||
buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildFormattedTextItem(messageContent: MessageTextContent,
|
||||
private fun buildFormattedTextItem(matrixFormattedBody: String,
|
||||
informationData: MessageInformationData,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?,
|
||||
attributes: AbsMessageItem.Attributes): MessageTextItem? {
|
||||
val compressed = htmlCompressor.compress(messageContent.formattedBody!!)
|
||||
val formattedBody = htmlRenderer.get().render(compressed, pillsPostProcessor)
|
||||
return buildMessageTextItem(formattedBody, true, informationData, highlight, callback, attributes)
|
||||
val compressed = htmlCompressor.compress(matrixFormattedBody)
|
||||
val renderedFormattedBody = htmlRenderer.get().render(compressed, pillsPostProcessor) as Spanned
|
||||
return buildMessageTextItem(renderedFormattedBody, true, informationData, highlight, callback, attributes)
|
||||
}
|
||||
|
||||
private fun buildMessageTextItem(body: CharSequence,
|
||||
|
@ -526,24 +498,6 @@ class MessageItemFactory @Inject constructor(
|
|||
.movementMethod(createLinkMovementMethod(callback))
|
||||
}
|
||||
|
||||
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.toEpoxyCharSequence())
|
||||
}
|
||||
}
|
||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||
.attributes(attributes)
|
||||
.highlighted(highlight)
|
||||
.message(formattedBody.toEpoxyCharSequence())
|
||||
}
|
||||
|
||||
private fun annotateWithEdited(linkifiedBody: CharSequence,
|
||||
callback: TimelineEventController.Callback?,
|
||||
informationData: MessageInformationData): Spannable {
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
/*
|
||||
* 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.app.features.home.room.detail.timeline.item
|
||||
|
||||
import android.widget.TextView
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence
|
||||
import im.vector.app.core.epoxy.onClick
|
||||
import im.vector.app.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: EpoxyCharSequence? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var editedSpan: EpoxyCharSequence? = null
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
holder.messageView.text = message?.charSequence
|
||||
renderSendState(holder.messageView, holder.messageView)
|
||||
holder.messageView.onClick(attributes.itemClickListener)
|
||||
holder.messageView.setOnLongClickListener(attributes.itemLongClickListener)
|
||||
holder.editedView.movementMethod = BetterLinkMovementMethod.getInstance()
|
||||
holder.editedView.setTextOrHide(editedSpan?.charSequence)
|
||||
}
|
||||
|
||||
override fun getViewStubId() = 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
|
||||
}
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
/*
|
||||
* 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.app.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
|
||||
}
|
||||
}
|
|
@ -121,6 +121,8 @@ class MatrixHtmlPluginConfigure @Inject constructor(private val colorProvider: C
|
|||
.addHandler(FontTagHandler())
|
||||
.addHandler(ParagraphHandler(DimensionConverter(resources)))
|
||||
.addHandler(MxReplyTagHandler())
|
||||
.addHandler(CodePreTagHandler())
|
||||
.addHandler(CodeTagHandler())
|
||||
.addHandler(SpanHandler(colorProvider))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.html
|
||||
|
||||
import io.noties.markwon.MarkwonVisitor
|
||||
import io.noties.markwon.SpannableBuilder
|
||||
import io.noties.markwon.html.HtmlTag
|
||||
import io.noties.markwon.html.MarkwonHtmlRenderer
|
||||
import io.noties.markwon.html.TagHandler
|
||||
|
||||
class CodeTagHandler : TagHandler() {
|
||||
|
||||
override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) {
|
||||
SpannableBuilder.setSpans(
|
||||
visitor.builder(),
|
||||
HtmlCodeSpan(visitor.configuration().theme(), false),
|
||||
tag.start(),
|
||||
tag.end()
|
||||
)
|
||||
}
|
||||
|
||||
override fun supportedTags(): List<String> {
|
||||
return listOf("code")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre tag are already handled by HtmlPlugin to keep the formatting.
|
||||
* We are only using it to check for <pre><code>*</code></pre> tags.
|
||||
*/
|
||||
class CodePreTagHandler : TagHandler() {
|
||||
override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) {
|
||||
val htmlCodeSpan = visitor.builder()
|
||||
.getSpans(tag.start(), tag.end())
|
||||
.firstOrNull {
|
||||
it.what is HtmlCodeSpan
|
||||
}
|
||||
if (htmlCodeSpan != null) {
|
||||
(htmlCodeSpan.what as HtmlCodeSpan).isBlock = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun supportedTags(): List<String> {
|
||||
return listOf("pre")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.html
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import android.text.Layout
|
||||
import android.text.TextPaint
|
||||
import android.text.style.LeadingMarginSpan
|
||||
import android.text.style.MetricAffectingSpan
|
||||
import io.noties.markwon.core.MarkwonTheme
|
||||
|
||||
class HtmlCodeSpan(private val theme: MarkwonTheme, var isBlock: Boolean) : MetricAffectingSpan(), LeadingMarginSpan {
|
||||
|
||||
private val rect = Rect()
|
||||
private val paint = Paint()
|
||||
|
||||
override fun updateDrawState(p: TextPaint) {
|
||||
applyTextStyle(p)
|
||||
if (!isBlock) {
|
||||
p.bgColor = theme.getCodeBackgroundColor(p)
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateMeasureState(p: TextPaint) {
|
||||
applyTextStyle(p)
|
||||
}
|
||||
|
||||
private fun applyTextStyle(p: TextPaint) {
|
||||
if (isBlock) {
|
||||
theme.applyCodeBlockTextStyle(p)
|
||||
} else {
|
||||
theme.applyCodeTextStyle(p)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLeadingMargin(first: Boolean): Int {
|
||||
return theme.codeBlockMargin
|
||||
}
|
||||
|
||||
override fun drawLeadingMargin(
|
||||
c: Canvas,
|
||||
p: Paint?,
|
||||
x: Int,
|
||||
dir: Int,
|
||||
top: Int,
|
||||
baseline: Int,
|
||||
bottom: Int,
|
||||
text: CharSequence?,
|
||||
start: Int,
|
||||
end: Int,
|
||||
first: Boolean,
|
||||
layout: Layout?
|
||||
) {
|
||||
if (!isBlock) return
|
||||
|
||||
paint.style = Paint.Style.FILL
|
||||
paint.color = theme.getCodeBlockBackgroundColor(p!!)
|
||||
val left: Int
|
||||
val right: Int
|
||||
if (dir > 0) {
|
||||
left = x
|
||||
right = c.width
|
||||
} else {
|
||||
left = x - c.width
|
||||
right = x
|
||||
}
|
||||
rect[left, top, right] = bottom
|
||||
c.drawRect(rect, paint)
|
||||
}
|
||||
}
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
package im.vector.app.features.html
|
||||
|
||||
import android.content.res.Resources
|
||||
import im.vector.app.core.utils.DimensionConverter
|
||||
import io.noties.markwon.MarkwonVisitor
|
||||
import io.noties.markwon.SpannableBuilder
|
||||
|
@ -24,7 +23,6 @@ import io.noties.markwon.html.HtmlTag
|
|||
import io.noties.markwon.html.MarkwonHtmlRenderer
|
||||
import io.noties.markwon.html.TagHandler
|
||||
import me.gujun.android.span.style.VerticalPaddingSpan
|
||||
import org.commonmark.node.BlockQuote
|
||||
|
||||
class ParagraphHandler(private val dimensionConverter: DimensionConverter) : TagHandler() {
|
||||
|
||||
|
@ -34,15 +32,11 @@ class ParagraphHandler(private val dimensionConverter: DimensionConverter) : Tag
|
|||
if (tag.isBlock) {
|
||||
visitChildren(visitor, renderer, tag.asBlock)
|
||||
}
|
||||
val configuration = visitor.configuration()
|
||||
val factory = configuration.spansFactory().get(BlockQuote::class.java)
|
||||
if (factory != null) {
|
||||
SpannableBuilder.setSpans(
|
||||
visitor.builder(),
|
||||
VerticalPaddingSpan(dimensionConverter.dpToPx(16), 0),
|
||||
tag.start(),
|
||||
tag.end()
|
||||
)
|
||||
}
|
||||
SpannableBuilder.setSpans(
|
||||
visitor.builder(),
|
||||
VerticalPaddingSpan(dimensionConverter.dpToPx(4), dimensionConverter.dpToPx(4)),
|
||||
tag.start(),
|
||||
tag.end()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue