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.TimelineMediaSizeProvider
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker
|
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.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.MessageFileItem_
|
import im.vector.app.features.home.room.detail.timeline.item.MessageFileItem_
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.MessageImageVideoItem
|
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.item.VerificationRequestItem_
|
||||||
import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod
|
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.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.EventHtmlRenderer
|
||||||
import im.vector.app.features.html.PillsPostProcessor
|
import im.vector.app.features.html.PillsPostProcessor
|
||||||
import im.vector.app.features.html.SpanUtils
|
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.ImageContentRenderer
|
||||||
import im.vector.app.features.media.VideoContentRenderer
|
import im.vector.app.features.media.VideoContentRenderer
|
||||||
import me.gujun.android.span.span
|
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.MatrixUrls.isMxcUrl
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
@ -454,46 +450,22 @@ class MessageItemFactory @Inject constructor(
|
|||||||
highlight: Boolean,
|
highlight: Boolean,
|
||||||
callback: TimelineEventController.Callback?,
|
callback: TimelineEventController.Callback?,
|
||||||
attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? {
|
attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? {
|
||||||
val isFormatted = messageContent.matrixFormattedBody.isNullOrBlank().not()
|
val matrixFormattedBody = messageContent.matrixFormattedBody
|
||||||
return if (isFormatted) {
|
return if (matrixFormattedBody != null) {
|
||||||
// First detect if the message contains some code block(s) or inline code
|
buildFormattedTextItem(matrixFormattedBody, informationData, highlight, callback, attributes)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes)
|
buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildFormattedTextItem(messageContent: MessageTextContent,
|
private fun buildFormattedTextItem(matrixFormattedBody: String,
|
||||||
informationData: MessageInformationData,
|
informationData: MessageInformationData,
|
||||||
highlight: Boolean,
|
highlight: Boolean,
|
||||||
callback: TimelineEventController.Callback?,
|
callback: TimelineEventController.Callback?,
|
||||||
attributes: AbsMessageItem.Attributes): MessageTextItem? {
|
attributes: AbsMessageItem.Attributes): MessageTextItem? {
|
||||||
val compressed = htmlCompressor.compress(messageContent.formattedBody!!)
|
val compressed = htmlCompressor.compress(matrixFormattedBody)
|
||||||
val formattedBody = htmlRenderer.get().render(compressed, pillsPostProcessor)
|
val renderedFormattedBody = htmlRenderer.get().render(compressed, pillsPostProcessor) as Spanned
|
||||||
return buildMessageTextItem(formattedBody, true, informationData, highlight, callback, attributes)
|
return buildMessageTextItem(renderedFormattedBody, true, informationData, highlight, callback, attributes)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildMessageTextItem(body: CharSequence,
|
private fun buildMessageTextItem(body: CharSequence,
|
||||||
@ -526,24 +498,6 @@ class MessageItemFactory @Inject constructor(
|
|||||||
.movementMethod(createLinkMovementMethod(callback))
|
.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,
|
private fun annotateWithEdited(linkifiedBody: CharSequence,
|
||||||
callback: TimelineEventController.Callback?,
|
callback: TimelineEventController.Callback?,
|
||||||
informationData: MessageInformationData): Spannable {
|
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(FontTagHandler())
|
||||||
.addHandler(ParagraphHandler(DimensionConverter(resources)))
|
.addHandler(ParagraphHandler(DimensionConverter(resources)))
|
||||||
.addHandler(MxReplyTagHandler())
|
.addHandler(MxReplyTagHandler())
|
||||||
|
.addHandler(CodePreTagHandler())
|
||||||
|
.addHandler(CodeTagHandler())
|
||||||
.addHandler(SpanHandler(colorProvider))
|
.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
|
package im.vector.app.features.html
|
||||||
|
|
||||||
import android.content.res.Resources
|
|
||||||
import im.vector.app.core.utils.DimensionConverter
|
import im.vector.app.core.utils.DimensionConverter
|
||||||
import io.noties.markwon.MarkwonVisitor
|
import io.noties.markwon.MarkwonVisitor
|
||||||
import io.noties.markwon.SpannableBuilder
|
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.MarkwonHtmlRenderer
|
||||||
import io.noties.markwon.html.TagHandler
|
import io.noties.markwon.html.TagHandler
|
||||||
import me.gujun.android.span.style.VerticalPaddingSpan
|
import me.gujun.android.span.style.VerticalPaddingSpan
|
||||||
import org.commonmark.node.BlockQuote
|
|
||||||
|
|
||||||
class ParagraphHandler(private val dimensionConverter: DimensionConverter) : TagHandler() {
|
class ParagraphHandler(private val dimensionConverter: DimensionConverter) : TagHandler() {
|
||||||
|
|
||||||
@ -34,15 +32,11 @@ class ParagraphHandler(private val dimensionConverter: DimensionConverter) : Tag
|
|||||||
if (tag.isBlock) {
|
if (tag.isBlock) {
|
||||||
visitChildren(visitor, renderer, tag.asBlock)
|
visitChildren(visitor, renderer, tag.asBlock)
|
||||||
}
|
}
|
||||||
val configuration = visitor.configuration()
|
SpannableBuilder.setSpans(
|
||||||
val factory = configuration.spansFactory().get(BlockQuote::class.java)
|
visitor.builder(),
|
||||||
if (factory != null) {
|
VerticalPaddingSpan(dimensionConverter.dpToPx(4), dimensionConverter.dpToPx(4)),
|
||||||
SpannableBuilder.setSpans(
|
tag.start(),
|
||||||
visitor.builder(),
|
tag.end()
|
||||||
VerticalPaddingSpan(dimensionConverter.dpToPx(16), 0),
|
)
|
||||||
tag.start(),
|
|
||||||
tag.end()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user