From 351787315676f7ed5dfe0e19f82fa1371a6ccfc1 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 29 Oct 2019 19:08:48 +0100 Subject: [PATCH 1/5] Timeline: Start handling code blocks. [WIP] --- vector/build.gradle | 6 +- .../timeline/factory/MessageItemFactory.kt | 37 ++-- .../timeline/item/MessageBlockCodeItem.kt | 44 +++++ .../vector/riotx/features/html/CodeVisitor.kt | 56 ++++++ .../riotx/features/html/EventHtmlRenderer.kt | 164 +++--------------- .../riotx/features/html/FontTagHandler.kt | 43 ++--- .../riotx/features/html/MxLinkTagHandler.kt | 65 +++++++ .../riotx/features/html/MxReplyTagHandler.kt | 44 +++++ .../res/layout/item_timeline_event_base.xml | 8 + .../item_timeline_event_code_block_stub.xml | 40 +++++ 10 files changed, 328 insertions(+), 179 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/html/CodeVisitor.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/html/MxReplyTagHandler.kt create mode 100644 vector/src/main/res/layout/item_timeline_event_code_block_stub.xml diff --git a/vector/build.gradle b/vector/build.gradle index 34c01c028e..d639b4c3e8 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -219,7 +219,7 @@ dependencies { def epoxy_version = '3.8.0' def arrow_version = "0.8.2" def coroutines_version = "1.3.2" - def markwon_version = '3.1.0' + def markwon_version = '4.1.2' def big_image_viewer_version = '1.5.6' def glide_version = '4.10.0' def moshi_version = '1.8.0' @@ -283,8 +283,8 @@ dependencies { implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' implementation 'com.google.android.material:material:1.1.0-beta01' implementation 'me.gujun.android:span:1.7' - implementation "ru.noties.markwon:core:$markwon_version" - implementation "ru.noties.markwon:html:$markwon_version" + implementation "io.noties.markwon:core:$markwon_version" + implementation "io.noties.markwon:html:$markwon_version" implementation 'me.saket:better-link-movement-method:2.2.0' implementation 'com.google.android:flexbox:1.1.1' diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 0bb5c3a1d8..cc00ba9fb6 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -46,9 +46,11 @@ import im.vector.riotx.features.home.room.detail.timeline.TimelineEventControlle import im.vector.riotx.features.home.room.detail.timeline.helper.* import im.vector.riotx.features.home.room.detail.timeline.item.* import im.vector.riotx.features.html.EventHtmlRenderer +import im.vector.riotx.features.html.CodeVisitor import im.vector.riotx.features.media.ImageContentRenderer import im.vector.riotx.features.media.VideoContentRenderer import me.gujun.android.span.span +import org.commonmark.node.Document import javax.inject.Inject class MessageItemFactory @Inject constructor( @@ -234,22 +236,33 @@ class MessageItemFactory @Inject constructor( highlight: Boolean, callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes): MessageTextItem? { + val isFormatted = messageContent.formattedBody.isNullOrBlank().not() - val bodyToUse = messageContent.formattedBody?.let { - htmlRenderer.get().render(it.trim()) - } ?: messageContent.body + val bodyToUse = if (isFormatted) { + val formattedBody = htmlRenderer.get().parse(messageContent.body) as Document + val codeVisitor = CodeVisitor() + codeVisitor.visit(formattedBody) + if (codeVisitor.codeKind == CodeVisitor.Kind.NONE) { + messageContent.formattedBody.let { + htmlRenderer.get().render(it!!.trim()) + } + } else { + htmlRenderer.get().render(formattedBody) + } + } else { + messageContent.body + } val linkifiedBody = linkifyBody(bodyToUse, callback) - return MessageTextItem_() - .apply { - if (informationData.hasBeenEdited) { - val spannable = annotateWithEdited(linkifiedBody, callback, informationData) - message(spannable) - } else { - message(linkifiedBody) - } - } + return MessageTextItem_().apply { + if (informationData.hasBeenEdited) { + val spannable = annotateWithEdited(linkifiedBody, callback, informationData) + message(spannable) + } else { + message(linkifiedBody) + } + } .useBigFont(linkifiedBody.length <= MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT * 2 && containsOnlyEmojis(linkifiedBody.toString())) .searchForPills(isFormatted) .leftGuideline(avatarSizeProvider.leftGuideline) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt new file mode 100644 index 0000000000..1ac35181ce --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt @@ -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() { + + @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 + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/html/CodeVisitor.kt b/vector/src/main/java/im/vector/riotx/features/html/CodeVisitor.kt new file mode 100644 index 0000000000..009d74818a --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/html/CodeVisitor.kt @@ -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 + } + +} diff --git a/vector/src/main/java/im/vector/riotx/features/html/EventHtmlRenderer.kt b/vector/src/main/java/im/vector/riotx/features/html/EventHtmlRenderer.kt index 06af8ebca5..4e387a5b12 100644 --- a/vector/src/main/java/im/vector/riotx/features/html/EventHtmlRenderer.kt +++ b/vector/src/main/java/im/vector/riotx/features/html/EventHtmlRenderer.kt @@ -17,171 +17,47 @@ package im.vector.riotx.features.html import android.content.Context -import android.text.style.URLSpan -import im.vector.matrix.android.api.permalinks.PermalinkData -import im.vector.matrix.android.api.permalinks.PermalinkParser import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.glide.GlideApp -import im.vector.riotx.core.glide.GlideRequests import im.vector.riotx.features.home.AvatarRenderer -import org.commonmark.node.BlockQuote -import org.commonmark.node.HtmlBlock -import org.commonmark.node.HtmlInline +import io.noties.markwon.Markwon +import io.noties.markwon.html.HtmlPlugin +import io.noties.markwon.html.TagHandlerNoOp import org.commonmark.node.Node -import ru.noties.markwon.* -import ru.noties.markwon.html.HtmlTag -import ru.noties.markwon.html.MarkwonHtmlParserImpl -import ru.noties.markwon.html.MarkwonHtmlRenderer -import ru.noties.markwon.html.TagHandler -import ru.noties.markwon.html.tag.* -import java.util.Arrays.asList import javax.inject.Inject import javax.inject.Singleton @Singleton class EventHtmlRenderer @Inject constructor(context: Context, - avatarRenderer: AvatarRenderer, - sessionHolder: ActiveSessionHolder) { + htmlConfigure: MatrixHtmlPluginConfigure) { + private val markwon = Markwon.builder(context) - .usePlugin(MatrixPlugin.create(GlideApp.with(context), context, avatarRenderer, sessionHolder)) + .usePlugin(HtmlPlugin.create(htmlConfigure)) .build() + fun parse(text: String): Node { + return markwon.parse(text) + } + fun render(text: String): CharSequence { return markwon.toMarkdown(text) } - fun render(node: Node) : CharSequence { + fun render(node: Node): CharSequence { return markwon.render(node) } } -private class MatrixPlugin private constructor(private val glideRequests: GlideRequests, - private val context: Context, - private val avatarRenderer: AvatarRenderer, - private val session: ActiveSessionHolder) : AbstractMarkwonPlugin() { +class MatrixHtmlPluginConfigure @Inject constructor(private val context: Context, + private val avatarRenderer: AvatarRenderer, + private val session: ActiveSessionHolder) : HtmlPlugin.HtmlConfigure { - override fun configureConfiguration(builder: MarkwonConfiguration.Builder) { - builder.htmlParser(MarkwonHtmlParserImpl.create()) + override fun configureHtml(plugin: HtmlPlugin) { + plugin + .addHandler(TagHandlerNoOp.create("a")) + .addHandler(FontTagHandler()) + .addHandler(MxLinkTagHandler(GlideApp.with(context), context, avatarRenderer, session)) + .addHandler(MxReplyTagHandler()) } - override fun configureHtmlRenderer(builder: MarkwonHtmlRenderer.Builder) { - builder - .setHandler( - "img", - ImageHandler.create()) - .setHandler( - "a", - MxLinkHandler(glideRequests, context, avatarRenderer, session)) - .setHandler( - "blockquote", - BlockquoteHandler()) - .setHandler( - "font", - FontTagHandler()) - .setHandler( - "sub", - SubScriptHandler()) - .setHandler( - "sup", - SuperScriptHandler()) - .setHandler( - asList("b", "strong"), - StrongEmphasisHandler()) - .setHandler( - asList("s", "del"), - StrikeHandler()) - .setHandler( - asList("u", "ins"), - UnderlineHandler()) - .setHandler( - asList("ul", "ol"), - ListHandler()) - .setHandler( - asList("i", "em", "cite", "dfn"), - EmphasisHandler()) - .setHandler( - asList("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) - } - } } diff --git a/vector/src/main/java/im/vector/riotx/features/html/FontTagHandler.kt b/vector/src/main/java/im/vector/riotx/features/html/FontTagHandler.kt index f4fa1737c9..e5733dd849 100644 --- a/vector/src/main/java/im/vector/riotx/features/html/FontTagHandler.kt +++ b/vector/src/main/java/im/vector/riotx/features/html/FontTagHandler.kt @@ -17,15 +17,18 @@ package im.vector.riotx.features.html import android.graphics.Color import android.text.style.ForegroundColorSpan -import ru.noties.markwon.MarkwonConfiguration -import ru.noties.markwon.RenderProps -import ru.noties.markwon.html.HtmlTag -import ru.noties.markwon.html.tag.SimpleTagHandler +import io.noties.markwon.MarkwonConfiguration +import io.noties.markwon.RenderProps +import io.noties.markwon.html.HtmlTag +import io.noties.markwon.html.tag.SimpleTagHandler /** * custom to matrix for IRC-style font coloring */ class FontTagHandler : SimpleTagHandler() { + + override fun supportedTags() = listOf("font") + override fun getSpans(configuration: MarkwonConfiguration, renderProps: RenderProps, tag: HtmlTag): Any? { val colorString = tag.attributes()["color"]?.let { parseColor(it) } ?: Color.BLACK return ForegroundColorSpan(colorString) @@ -37,23 +40,23 @@ class FontTagHandler : SimpleTagHandler() { } catch (e: Exception) { // try other w3c colors? return when (color_name) { - "white" -> Color.WHITE - "yellow" -> Color.YELLOW + "white" -> Color.WHITE + "yellow" -> Color.YELLOW "fuchsia" -> Color.parseColor("#FF00FF") - "red" -> Color.RED - "silver" -> Color.parseColor("#C0C0C0") - "gray" -> Color.GRAY - "olive" -> Color.parseColor("#808000") - "purple" -> Color.parseColor("#800080") - "maroon" -> Color.parseColor("#800000") - "aqua" -> Color.parseColor("#00FFFF") - "lime" -> Color.parseColor("#00FF00") - "teal" -> Color.parseColor("#008080") - "green" -> Color.GREEN - "blue" -> Color.BLUE - "orange" -> Color.parseColor("#FFA500") - "navy" -> Color.parseColor("#000080") - else -> Color.BLACK + "red" -> Color.RED + "silver" -> Color.parseColor("#C0C0C0") + "gray" -> Color.GRAY + "olive" -> Color.parseColor("#808000") + "purple" -> Color.parseColor("#800080") + "maroon" -> Color.parseColor("#800000") + "aqua" -> Color.parseColor("#00FFFF") + "lime" -> Color.parseColor("#00FF00") + "teal" -> Color.parseColor("#008080") + "green" -> Color.GREEN + "blue" -> Color.BLUE + "orange" -> Color.parseColor("#FFA500") + "navy" -> Color.parseColor("#000080") + else -> Color.BLACK } } } diff --git a/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt b/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt new file mode 100644 index 0000000000..fdcbb12cd7 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt @@ -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) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/html/MxReplyTagHandler.kt b/vector/src/main/java/im/vector/riotx/features/html/MxReplyTagHandler.kt new file mode 100644 index 0000000000..f999e253c7 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/html/MxReplyTagHandler.kt @@ -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) + } + } +} diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml index fbe3b70551..260c309f6f 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -78,6 +78,14 @@ android:layout="@layout/item_timeline_event_text_message_stub" tools:visibility="visible" /> + + + + + + + + + + + + + + + + From b4ae331086db25b09b753bb89df3c3af6f8472c2 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 30 Oct 2019 19:00:00 +0100 Subject: [PATCH 2/5] Timeline: render inline and block code --- .../core/platform/MaxHeightScrollView.kt | 31 +------ .../home/room/detail/RoomDetailFragment.kt | 2 +- .../timeline/factory/MessageItemFactory.kt | 85 ++++++++++++------- .../timeline/item/MessageBlockCodeItem.kt | 18 +++- .../main/res/layout/fragment_room_detail.xml | 2 +- .../item_timeline_event_code_block_stub.xml | 54 +++++------- 6 files changed, 97 insertions(+), 95 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt b/vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt index fc09ad0f75..b8587750a3 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt @@ -16,17 +16,15 @@ package im.vector.riotx.core.platform -import android.annotation.TargetApi import android.content.Context -import android.os.Build import android.util.AttributeSet -import android.widget.ScrollView - +import androidx.core.widget.NestedScrollView import im.vector.riotx.R 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 set(value) { @@ -34,28 +32,7 @@ class MaxHeightScrollView : ScrollView { requestLayout() } - constructor(context: Context) : super(context) {} - - 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?) { + init { if (attrs != null) { val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.MaxHeightScrollView) maxHeight = styledAttrs.getDimensionPixelSize(R.styleable.MaxHeightScrollView_maxHeight, DEFAULT_MAX_HEIGHT) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 7c4437d6f0..9a83b331c3 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -480,7 +480,7 @@ class RoomDetailFragment : jumpToReadMarkerView.render(show, readMarkerId) } } - recyclerView.setController(timelineEventController) + recyclerView.adapter = timelineEventController.adapter recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { if (recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index cc00ba9fb6..364d0285cc 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -99,16 +99,8 @@ class MessageItemFactory @Inject constructor( // val all = event.root.toContent() // val ev = all.toModel() return when (messageContent) { - is MessageEmoteContent -> buildEmoteMessageItem(messageContent, - informationData, - highlight, - callback, - attributes) - is MessageTextContent -> buildTextMessageItem(messageContent, - informationData, - highlight, - callback, - attributes) + is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes) is MessageImageContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes) @@ -231,29 +223,43 @@ class MessageItemFactory @Inject constructor( .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, highlight: Boolean, callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes): MessageTextItem? { - - val isFormatted = messageContent.formattedBody.isNullOrBlank().not() - val bodyToUse = if (isFormatted) { - val formattedBody = htmlRenderer.get().parse(messageContent.body) as Document - val codeVisitor = CodeVisitor() - codeVisitor.visit(formattedBody) - if (codeVisitor.codeKind == CodeVisitor.Kind.NONE) { - messageContent.formattedBody.let { - htmlRenderer.get().render(it!!.trim()) - } - } else { - htmlRenderer.get().render(formattedBody) - } - } else { - messageContent.body - } - - val linkifiedBody = linkifyBody(bodyToUse, callback) + val linkifiedBody = linkifyBody(body, callback) return MessageTextItem_().apply { if (informationData.hasBeenEdited) { @@ -269,7 +275,26 @@ class MessageItemFactory @Inject constructor( .attributes(attributes) .highlighted(highlight) .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, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt index 1ac35181ce..62042eb54d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt @@ -16,26 +16,38 @@ package im.vector.riotx.features.home.room.detail.timeline.item +import android.widget.TextView +import androidx.core.view.isVisible 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 +import im.vector.riotx.core.extensions.setTextOrHide +import me.saket.bettermovementmethod.BetterLinkMovementMethod + @EpoxyModelClass(layout = R.layout.item_timeline_event_base) abstract class MessageBlockCodeItem : AbsMessageItem() { @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(R.id.codeBlockTextView) + val editedView by bind(R.id.codeBlockEditedView) } companion object { diff --git a/vector/src/main/res/layout/fragment_room_detail.xml b/vector/src/main/res/layout/fragment_room_detail.xml index ab2c40c313..6661674edb 100644 --- a/vector/src/main/res/layout/fragment_room_detail.xml +++ b/vector/src/main/res/layout/fragment_room_detail.xml @@ -86,7 +86,7 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/roomToolbar" /> - - + android:orientation="vertical"> - + + + + + + + android:layout_marginTop="4dp" /> - - - - - - - - - + \ No newline at end of file From 5ab31a0ef5130a579c404f6132314836e437ed80 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 30 Oct 2019 19:00:56 +0100 Subject: [PATCH 3/5] Fix klint --- .../home/room/detail/timeline/factory/MessageItemFactory.kt | 3 --- .../home/room/detail/timeline/item/MessageBlockCodeItem.kt | 2 -- .../src/main/java/im/vector/riotx/features/html/CodeVisitor.kt | 1 - .../java/im/vector/riotx/features/html/EventHtmlRenderer.kt | 1 - 4 files changed, 7 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 364d0285cc..eacd702b7d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -228,7 +228,6 @@ class MessageItemFactory @Inject constructor( 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 @@ -282,8 +281,6 @@ class MessageItemFactory @Inject constructor( highlight: Boolean, callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes): MessageBlockCodeItem? { - - return MessageBlockCodeItem_() .apply { if (informationData.hasBeenEdited) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt index 62042eb54d..82a6a4db6f 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt @@ -17,14 +17,12 @@ package im.vector.riotx.features.home.room.detail.timeline.item import android.widget.TextView -import androidx.core.view.isVisible 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() { diff --git a/vector/src/main/java/im/vector/riotx/features/html/CodeVisitor.kt b/vector/src/main/java/im/vector/riotx/features/html/CodeVisitor.kt index 009d74818a..ed8db94fc3 100644 --- a/vector/src/main/java/im/vector/riotx/features/html/CodeVisitor.kt +++ b/vector/src/main/java/im/vector/riotx/features/html/CodeVisitor.kt @@ -52,5 +52,4 @@ class CodeVisitor : AbstractVisitor() { INLINE, BLOCK } - } diff --git a/vector/src/main/java/im/vector/riotx/features/html/EventHtmlRenderer.kt b/vector/src/main/java/im/vector/riotx/features/html/EventHtmlRenderer.kt index 4e387a5b12..dc9e21e440 100644 --- a/vector/src/main/java/im/vector/riotx/features/html/EventHtmlRenderer.kt +++ b/vector/src/main/java/im/vector/riotx/features/html/EventHtmlRenderer.kt @@ -59,5 +59,4 @@ class MatrixHtmlPluginConfigure @Inject constructor(private val context: Context .addHandler(MxLinkTagHandler(GlideApp.with(context), context, avatarRenderer, session)) .addHandler(MxReplyTagHandler()) } - } From 30b2e530029d97c93dc118eeb7bb8a4bcdb545da Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 30 Oct 2019 19:02:44 +0100 Subject: [PATCH 4/5] Update CHANGES --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 50e8a8e94a..b39140aa2d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,7 +5,7 @@ Features ✨: - Improvements 🙌: - - + - Handle code tags (#567) Other changes: - From 3483debcc177b22e07b7b185975f03da36445ce4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 31 Oct 2019 12:08:55 +0100 Subject: [PATCH 5/5] Little cleanup --- vector/src/main/res/layout/item_timeline_event_base.xml | 1 - .../src/main/res/layout/item_timeline_event_code_block_stub.xml | 1 - 2 files changed, 2 deletions(-) diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml index 260c309f6f..50ed0aae23 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -82,7 +82,6 @@ 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" /> diff --git a/vector/src/main/res/layout/item_timeline_event_code_block_stub.xml b/vector/src/main/res/layout/item_timeline_event_code_block_stub.xml index 804c02940a..4738f331b1 100644 --- a/vector/src/main/res/layout/item_timeline_event_code_block_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_code_block_stub.xml @@ -13,7 +13,6 @@ android:id="@+id/codeBlockTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="4dp" android:fontFamily="monospace" android:textSize="14sp" />