From 4458e28ce27c142a28ac87a5dd2fed521bb60131 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 21 Feb 2019 19:21:08 +0100 Subject: [PATCH 01/13] Html : introduce markown lib to handle html rendering. --- app/build.gradle | 4 ++ .../vector/riotredesign/core/di/AppModule.kt | 5 ++ .../riotredesign/features/home/HomeModule.kt | 2 +- .../detail/timeline/MessageItemFactory.kt | 14 ++++- .../features/markdown/HtmlRenderer.kt | 63 +++++++++++++++++++ build.gradle | 1 + 6 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/im/vector/riotredesign/features/markdown/HtmlRenderer.kt diff --git a/app/build.gradle b/app/build.gradle index 12b174e0b5..e91f74d450 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -60,6 +60,7 @@ dependencies { def epoxy_version = "3.0.0" def arrow_version = "0.8.2" + def markwon_version = '3.0.0-SNAPSHOT' implementation project(":matrix-sdk-android") implementation project(":matrix-sdk-android-rx") @@ -97,6 +98,9 @@ dependencies { implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' implementation 'com.google.android.material:material:1.1.0-alpha02' implementation 'me.gujun.android:span:1.7' + implementation "ru.noties.markwon:core:$markwon_version" + implementation "ru.noties.markwon:html:$markwon_version" + // DI implementation "org.koin:koin-android:$koin_version" diff --git a/app/src/main/java/im/vector/riotredesign/core/di/AppModule.kt b/app/src/main/java/im/vector/riotredesign/core/di/AppModule.kt index bbc973152c..b022525188 100644 --- a/app/src/main/java/im/vector/riotredesign/core/di/AppModule.kt +++ b/app/src/main/java/im/vector/riotredesign/core/di/AppModule.kt @@ -22,6 +22,7 @@ import im.vector.riotredesign.core.resources.ColorProvider import im.vector.riotredesign.core.resources.LocaleProvider import im.vector.riotredesign.core.resources.StringProvider import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository +import im.vector.riotredesign.features.markdown.HtmlRenderer import org.koin.dsl.module.module class AppModule(private val context: Context) { @@ -48,5 +49,9 @@ class AppModule(private val context: Context) { RoomSelectionRepository(get()) } + single { + HtmlRenderer(context) + } + } } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt b/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt index 2f2d85a026..1af5f66467 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt @@ -33,7 +33,7 @@ class HomeModule { } single { - MessageItemFactory(get(), get(), get()) + MessageItemFactory(get(), get(), get(), get()) } single { diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageItemFactory.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageItemFactory.kt index e2f61cf0c8..2aae81d899 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageItemFactory.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageItemFactory.kt @@ -29,12 +29,14 @@ import im.vector.riotredesign.core.epoxy.RiotEpoxyModel import im.vector.riotredesign.core.extensions.localDateTime import im.vector.riotredesign.core.resources.ColorProvider import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider +import im.vector.riotredesign.features.markdown.HtmlRenderer import im.vector.riotredesign.features.media.MediaContentRenderer import me.gujun.android.span.span class MessageItemFactory(private val colorProvider: ColorProvider, private val timelineMediaSizeProvider: TimelineMediaSizeProvider, - private val timelineDateFormatter: TimelineDateFormatter) { + private val timelineDateFormatter: TimelineDateFormatter, + private val htmlRenderer: HtmlRenderer) { private val messagesDisplayedWithInformation = HashSet() @@ -102,9 +104,15 @@ class MessageItemFactory(private val colorProvider: ColorProvider, informationData: MessageInformationData, callback: TimelineEventController.Callback?): MessageTextItem? { - val message = linkifyBody(messageContent.body, callback) + val bodyToUse = messageContent.formattedBody + ?.let { + htmlRenderer.render(it) + } + ?: messageContent.body + + val linkifiedBody = linkifyBody(bodyToUse, callback) return MessageTextItem_() - .message(message) + .message(linkifiedBody) .informationData(informationData) } diff --git a/app/src/main/java/im/vector/riotredesign/features/markdown/HtmlRenderer.kt b/app/src/main/java/im/vector/riotredesign/features/markdown/HtmlRenderer.kt new file mode 100644 index 0000000000..f3c659333a --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/markdown/HtmlRenderer.kt @@ -0,0 +1,63 @@ +/* + * + * * 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.riotredesign.features.markdown + +import android.content.Context +import ru.noties.markwon.AbstractMarkwonPlugin +import ru.noties.markwon.Markwon +import ru.noties.markwon.MarkwonVisitor +import ru.noties.markwon.html.HtmlPlugin +import ru.noties.markwon.html.HtmlTag +import ru.noties.markwon.html.MarkwonHtmlRenderer +import ru.noties.markwon.html.TagHandler +import timber.log.Timber + +class HtmlRenderer(private val context: Context) { + + private val markwon = Markwon.builder(context) + .usePlugin(HtmlPlugin.create()) + .usePlugin(MatrixPlugin.create()) + .build() + + fun render(text: String): CharSequence { + return markwon.toMarkdown(text) + } + +} + +private class MatrixPlugin private constructor() : AbstractMarkwonPlugin() { + + override fun configureHtmlRenderer(builder: MarkwonHtmlRenderer.Builder) { + builder.addHandler("mx-reply", MxReplyTagHandler()) + } + + companion object { + + fun create(): MatrixPlugin { + return MatrixPlugin() + } + } +} + +private class MxReplyTagHandler : TagHandler() { + override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) { + Timber.v("Handle mx-reply") + } + +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 7216b2637d..af25b46084 100644 --- a/build.gradle +++ b/build.gradle @@ -22,6 +22,7 @@ allprojects { google() jcenter() maven { url 'https://jitpack.io' } + maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } } } From d2db5e32fcace618556e79d6ac31b81d9c8487bd Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 22 Feb 2019 15:43:48 +0100 Subject: [PATCH 02/13] Html : start handling reply --- .../features/markdown/HtmlRenderer.kt | 97 +++++++++++++++++-- 1 file changed, 91 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/im/vector/riotredesign/features/markdown/HtmlRenderer.kt b/app/src/main/java/im/vector/riotredesign/features/markdown/HtmlRenderer.kt index f3c659333a..30f5030ea9 100644 --- a/app/src/main/java/im/vector/riotredesign/features/markdown/HtmlRenderer.kt +++ b/app/src/main/java/im/vector/riotredesign/features/markdown/HtmlRenderer.kt @@ -19,19 +19,35 @@ package im.vector.riotredesign.features.markdown import android.content.Context +import org.commonmark.node.BlockQuote +import org.commonmark.node.HtmlBlock +import org.commonmark.node.HtmlInline +import org.commonmark.node.Node import ru.noties.markwon.AbstractMarkwonPlugin import ru.noties.markwon.Markwon +import ru.noties.markwon.MarkwonConfiguration import ru.noties.markwon.MarkwonVisitor -import ru.noties.markwon.html.HtmlPlugin +import ru.noties.markwon.SpannableBuilder 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 timber.log.Timber +import ru.noties.markwon.html.tag.BlockquoteHandler +import ru.noties.markwon.html.tag.EmphasisHandler +import ru.noties.markwon.html.tag.HeadingHandler +import ru.noties.markwon.html.tag.ImageHandler +import ru.noties.markwon.html.tag.LinkHandler +import ru.noties.markwon.html.tag.ListHandler +import ru.noties.markwon.html.tag.StrikeHandler +import ru.noties.markwon.html.tag.StrongEmphasisHandler +import ru.noties.markwon.html.tag.SubScriptHandler +import ru.noties.markwon.html.tag.SuperScriptHandler +import ru.noties.markwon.html.tag.UnderlineHandler +import java.util.Arrays.asList class HtmlRenderer(private val context: Context) { private val markwon = Markwon.builder(context) - .usePlugin(HtmlPlugin.create()) .usePlugin(MatrixPlugin.create()) .build() @@ -43,8 +59,65 @@ class HtmlRenderer(private val context: Context) { private class MatrixPlugin private constructor() : AbstractMarkwonPlugin() { + override fun configureConfiguration(builder: MarkwonConfiguration.Builder) { + builder.htmlParser(MarkwonHtmlParserImpl.create()) + } + override fun configureHtmlRenderer(builder: MarkwonHtmlRenderer.Builder) { - builder.addHandler("mx-reply", MxReplyTagHandler()) + builder + .addHandler( + "img", + ImageHandler.create()) + .addHandler( + "a", + LinkHandler()) + .addHandler( + "blockquote", + BlockquoteHandler()) + .addHandler( + "sub", + SubScriptHandler()) + .addHandler( + "sup", + SuperScriptHandler()) + .addHandler( + asList("b", "strong"), + StrongEmphasisHandler()) + .addHandler( + asList("s", "del"), + StrikeHandler()) + .addHandler( + asList("u", "ins"), + UnderlineHandler()) + .addHandler( + asList("ul", "ol"), + ListHandler()) + .addHandler( + asList("i", "em", "cite", "dfn"), + EmphasisHandler()) + .addHandler( + asList("h1", "h2", "h3", "h4", "h5", "h6"), + HeadingHandler()) + .addHandler("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 { @@ -56,8 +129,20 @@ private class MatrixPlugin private constructor() : AbstractMarkwonPlugin() { } private class MxReplyTagHandler : TagHandler() { + override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) { - Timber.v("Handle mx-reply") + 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) + } } -} \ No newline at end of file +} From f06211ce4fb3059e44e65dac76d52495ecd2c7b8 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 25 Feb 2019 15:18:36 +0100 Subject: [PATCH 03/13] Start playing with ChipDrawable to show Pills --- .../vector/riotredesign/core/di/AppModule.kt | 5 -- .../features/home/HomeActivity.kt | 2 +- .../riotredesign/features/home/HomeModule.kt | 7 ++- .../detail/timeline/MessageItemFactory.kt | 4 +- .../{HtmlRenderer.kt => EventHtmlRenderer.kt} | 49 ++++++++++++++++--- app/src/main/res/values/colors.xml | 5 ++ app/src/main/res/values/themes_base.xml | 3 +- app/src/main/res/xml/pill_view.xml | 6 +++ 8 files changed, 64 insertions(+), 17 deletions(-) rename app/src/main/java/im/vector/riotredesign/features/markdown/{HtmlRenderer.kt => EventHtmlRenderer.kt} (72%) create mode 100644 app/src/main/res/xml/pill_view.xml diff --git a/app/src/main/java/im/vector/riotredesign/core/di/AppModule.kt b/app/src/main/java/im/vector/riotredesign/core/di/AppModule.kt index b022525188..bbc973152c 100644 --- a/app/src/main/java/im/vector/riotredesign/core/di/AppModule.kt +++ b/app/src/main/java/im/vector/riotredesign/core/di/AppModule.kt @@ -22,7 +22,6 @@ import im.vector.riotredesign.core.resources.ColorProvider import im.vector.riotredesign.core.resources.LocaleProvider import im.vector.riotredesign.core.resources.StringProvider import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository -import im.vector.riotredesign.features.markdown.HtmlRenderer import org.koin.dsl.module.module class AppModule(private val context: Context) { @@ -49,9 +48,5 @@ class AppModule(private val context: Context) { RoomSelectionRepository(get()) } - single { - HtmlRenderer(context) - } - } } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt b/app/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt index 087a00eaec..7f1e00d3ad 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt @@ -45,7 +45,7 @@ class HomeActivity : RiotActivity(), ToolbarConfigurable { private val homeNavigator by inject() override fun onCreate(savedInstanceState: Bundle?) { - loadKoinModules(listOf(HomeModule().definition)) + loadKoinModules(listOf(HomeModule(this).definition)) homeNavigator.activity = this super.onCreate(savedInstanceState) setContentView(R.layout.activity_home) diff --git a/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt b/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt index 1af5f66467..35c8276cd8 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt @@ -22,9 +22,10 @@ import im.vector.riotredesign.features.home.room.detail.timeline.* import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider import im.vector.riotredesign.features.home.room.list.RoomSummaryComparator import im.vector.riotredesign.features.home.room.list.RoomSummaryController +import im.vector.riotredesign.features.markdown.EventHtmlRenderer import org.koin.dsl.module.module -class HomeModule { +class HomeModule(homeActivity: HomeActivity) { val definition = module(override = true) { @@ -32,6 +33,10 @@ class HomeModule { TimelineDateFormatter(get()) } + single { + EventHtmlRenderer(homeActivity) + } + single { MessageItemFactory(get(), get(), get(), get()) } diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageItemFactory.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageItemFactory.kt index 2aae81d899..e1eaaecc6f 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageItemFactory.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageItemFactory.kt @@ -29,14 +29,14 @@ import im.vector.riotredesign.core.epoxy.RiotEpoxyModel import im.vector.riotredesign.core.extensions.localDateTime import im.vector.riotredesign.core.resources.ColorProvider import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider -import im.vector.riotredesign.features.markdown.HtmlRenderer +import im.vector.riotredesign.features.markdown.EventHtmlRenderer import im.vector.riotredesign.features.media.MediaContentRenderer import me.gujun.android.span.span class MessageItemFactory(private val colorProvider: ColorProvider, private val timelineMediaSizeProvider: TimelineMediaSizeProvider, private val timelineDateFormatter: TimelineDateFormatter, - private val htmlRenderer: HtmlRenderer) { + private val htmlRenderer: EventHtmlRenderer) { private val messagesDisplayedWithInformation = HashSet() diff --git a/app/src/main/java/im/vector/riotredesign/features/markdown/HtmlRenderer.kt b/app/src/main/java/im/vector/riotredesign/features/markdown/EventHtmlRenderer.kt similarity index 72% rename from app/src/main/java/im/vector/riotredesign/features/markdown/HtmlRenderer.kt rename to app/src/main/java/im/vector/riotredesign/features/markdown/EventHtmlRenderer.kt index 30f5030ea9..206d4282be 100644 --- a/app/src/main/java/im/vector/riotredesign/features/markdown/HtmlRenderer.kt +++ b/app/src/main/java/im/vector/riotredesign/features/markdown/EventHtmlRenderer.kt @@ -19,6 +19,11 @@ package im.vector.riotredesign.features.markdown import android.content.Context +import android.text.style.ImageSpan +import com.google.android.material.chip.ChipDrawable +import im.vector.matrix.android.api.permalinks.PermalinkData +import im.vector.matrix.android.api.permalinks.PermalinkParser +import im.vector.riotredesign.R import org.commonmark.node.BlockQuote import org.commonmark.node.HtmlBlock import org.commonmark.node.HtmlInline @@ -45,10 +50,10 @@ import ru.noties.markwon.html.tag.SuperScriptHandler import ru.noties.markwon.html.tag.UnderlineHandler import java.util.Arrays.asList -class HtmlRenderer(private val context: Context) { +class EventHtmlRenderer(private val context: Context) { private val markwon = Markwon.builder(context) - .usePlugin(MatrixPlugin.create()) + .usePlugin(MatrixPlugin.create(context)) .build() fun render(text: String): CharSequence { @@ -57,7 +62,7 @@ class HtmlRenderer(private val context: Context) { } -private class MatrixPlugin private constructor() : AbstractMarkwonPlugin() { +private class MatrixPlugin private constructor(private val context: Context) : AbstractMarkwonPlugin() { override fun configureConfiguration(builder: MarkwonConfiguration.Builder) { builder.htmlParser(MarkwonHtmlParserImpl.create()) @@ -70,7 +75,7 @@ private class MatrixPlugin private constructor() : AbstractMarkwonPlugin() { ImageHandler.create()) .addHandler( "a", - LinkHandler()) + MxLinkHandler(context)) .addHandler( "blockquote", BlockquoteHandler()) @@ -122,12 +127,44 @@ private class MatrixPlugin private constructor() : AbstractMarkwonPlugin() { companion object { - fun create(): MatrixPlugin { - return MatrixPlugin() + fun create(context: Context): MatrixPlugin { + return MatrixPlugin(context) } } } +private class MxLinkHandler(private val context: Context) : 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 chipDrawable = ChipDrawable.createFromResource(context, R.xml.pill_view) + chipDrawable.setText(permalinkData.userId) + chipDrawable.textEndPadding = 8f + chipDrawable.textStartPadding = 8f + chipDrawable.setBounds(0, 0, chipDrawable.intrinsicWidth, (chipDrawable.intrinsicHeight / 1.5f).toInt()) + val span = ImageSpan(chipDrawable) + SpannableBuilder.setSpans( + visitor.builder(), + span, + 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) { diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 9797d9459d..90e3e22a0b 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -17,4 +17,9 @@ #ebedf8 #a5a5a5 #61708B + + #FFC7C7C7 + #FF999999 + #FFF56679 + diff --git a/app/src/main/res/values/themes_base.xml b/app/src/main/res/values/themes_base.xml index 04ea854aaf..257f8fc527 100644 --- a/app/src/main/res/values/themes_base.xml +++ b/app/src/main/res/values/themes_base.xml @@ -1,7 +1,7 @@ -