Merge pull request #161 from vector-im/feature/fix_timeline_clicks

Fix / click|longclick link interference
This commit is contained in:
Valere 2019-06-07 14:43:04 +02:00 committed by GitHub
commit d3518c4944
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 100 additions and 54 deletions

View File

@ -91,7 +91,7 @@ dependencies {
def moshi_version = '1.8.0' def moshi_version = '1.8.0'
def lifecycle_version = '2.0.0' def lifecycle_version = '2.0.0'
def coroutines_version = "1.0.1" def coroutines_version = "1.0.1"
def markwon_version = '3.0.0-SNAPSHOT' def markwon_version = '3.0.0'
implementation fileTree(dir: 'libs', include: ['*.aar']) implementation fileTree(dir: 'libs', include: ['*.aar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

View File

@ -17,9 +17,6 @@
package im.vector.matrix.android.api.permalinks package im.vector.matrix.android.api.permalinks
import android.text.Spannable import android.text.Spannable
import android.text.SpannableString
import android.text.method.LinkMovementMethod
import android.widget.TextView
import im.vector.matrix.android.api.MatrixPatterns import im.vector.matrix.android.api.MatrixPatterns
/** /**
@ -56,37 +53,5 @@ object MatrixLinkify {
} }
return hasMatch return hasMatch
} }
fun addLinks(textView: TextView, callback: MatrixPermalinkSpan.Callback?): Boolean {
val text = textView.text
if (text is Spannable) {
if (addLinks(text, callback)) {
addLinkMovementMethod(textView)
return true
}
return false
} else {
val spannableString = SpannableString.valueOf(text)
if (addLinks(spannableString, callback)) {
addLinkMovementMethod(textView)
textView.text = spannableString
return true
}
return false
}
}
/**
* Add linkMovementMethod on textview if not already set
* @param textView the textView on which the movementMethod is set
*/
fun addLinkMovementMethod(textView: TextView) {
val movementMethod = textView.movementMethod
if (movementMethod == null || movementMethod !is LinkMovementMethod) {
if (textView.linksClickable) {
textView.movementMethod = LinkMovementMethod.getInstance()
}
}
}
} }

View File

@ -132,7 +132,7 @@ dependencies {
def epoxy_version = "3.3.0" def epoxy_version = "3.3.0"
def arrow_version = "0.8.2" def arrow_version = "0.8.2"
def coroutines_version = "1.0.1" def coroutines_version = "1.0.1"
def markwon_version = '3.0.0-SNAPSHOT' def markwon_version = '3.0.0'
def big_image_viewer_version = '1.5.6' def big_image_viewer_version = '1.5.6'
def glide_version = '4.9.0' def glide_version = '4.9.0'
def moshi_version = '1.8.0' def moshi_version = '1.8.0'
@ -184,6 +184,7 @@ dependencies {
implementation 'me.gujun.android:span:1.7' implementation 'me.gujun.android:span:1.7'
implementation "ru.noties.markwon:core:$markwon_version" implementation "ru.noties.markwon:core:$markwon_version"
implementation "ru.noties.markwon:html:$markwon_version" implementation "ru.noties.markwon:html:$markwon_version"
implementation 'me.saket:better-link-movement-method:2.2.0'
implementation 'com.otaliastudios:autocomplete:1.1.0' implementation 'com.otaliastudios:autocomplete:1.1.0'

View File

@ -63,7 +63,7 @@ class MessageActionsViewModel(initialState: MessageActionState) : VectorViewMode
var body: CharSequence = messageContent?.body ?: "" var body: CharSequence = messageContent?.body ?: ""
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
val parser = Parser.builder().build() val parser = Parser.builder().build()
val document = parser.parse(messageContent.formattedBody ?: messageContent.body) val document = parser.parse(messageContent.formattedBody?.trim() ?: messageContent.body)
// val renderer = HtmlRenderer.builder().build() // val renderer = HtmlRenderer.builder().build()
body = Markwon.builder(viewModelContext.activity) body = Markwon.builder(viewModelContext.activity)
.usePlugin(HtmlPlugin.create()).build().render(document) .usePlugin(HtmlPlugin.create()).build().render(document)

View File

@ -295,7 +295,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
callback: TimelineEventController.Callback?): MessageTextItem? { callback: TimelineEventController.Callback?): MessageTextItem? {
val bodyToUse = messageContent.formattedBody?.let { val bodyToUse = messageContent.formattedBody?.let {
htmlRenderer.render(it) htmlRenderer.render(it.trim())
} ?: messageContent.body } ?: messageContent.body
val linkifiedBody = linkifyBody(bodyToUse, callback) val linkifiedBody = linkifyBody(bodyToUse, callback)
@ -319,11 +319,6 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
DebouncedClickListener(View.OnClickListener { view -> DebouncedClickListener(View.OnClickListener { view ->
callback?.onMemberNameClicked(informationData) callback?.onMemberNameClicked(informationData)
})) }))
//click on the text
.clickListener(
DebouncedClickListener(View.OnClickListener { view ->
callback?.onEventCellClicked(informationData, messageContent, view)
}))
.cellClickListener( .cellClickListener(
DebouncedClickListener(View.OnClickListener { view -> DebouncedClickListener(View.OnClickListener { view ->
callback?.onEventCellClicked(informationData, messageContent, view) callback?.onEventCellClicked(informationData, messageContent, view)

View File

@ -57,6 +57,7 @@ class TimelineItemFactory(private val messageItemFactory: MessageItemFactory,
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e,"failed to create message item")
defaultItemFactory.create(event, e) defaultItemFactory.create(event, e)
} }
return (computedModel ?: EmptyItem_()) return (computedModel ?: EmptyItem_())

View File

@ -43,6 +43,8 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
ContentUploadStateTrackerBinder.bind(informationData.eventId, mediaData, holder.progressLayout) ContentUploadStateTrackerBinder.bind(informationData.eventId, mediaData, holder.progressLayout)
holder.imageView.setOnClickListener(clickListener) holder.imageView.setOnClickListener(clickListener)
holder.imageView.setOnLongClickListener(longClickListener) holder.imageView.setOnLongClickListener(longClickListener)
holder.mediaContentView.setOnClickListener(cellClickListener)
holder.mediaContentView.setOnLongClickListener(longClickListener)
holder.imageView.renderSendState() holder.imageView.renderSendState()
holder.playContentView.visibility = if (playable) View.VISIBLE else View.GONE holder.playContentView.visibility = if (playable) View.VISIBLE else View.GONE
} }
@ -62,6 +64,8 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
val imageView by bind<ImageView>(R.id.messageThumbnailView) val imageView by bind<ImageView>(R.id.messageThumbnailView)
val playContentView by bind<ImageView>(R.id.messageMediaPlayView) val playContentView by bind<ImageView>(R.id.messageMediaPlayView)
val mediaContentView by bind<ViewGroup>(R.id.messageContentMedia)
} }

View File

@ -16,23 +16,19 @@
package im.vector.riotredesign.features.home.room.detail.timeline.item package im.vector.riotredesign.features.home.room.detail.timeline.item
import android.text.Spannable
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.widget.AppCompatTextView import androidx.appcompat.widget.AppCompatTextView
import androidx.core.text.PrecomputedTextCompat import androidx.core.text.PrecomputedTextCompat
import androidx.core.text.toSpannable import androidx.core.text.toSpannable
import androidx.core.widget.TextViewCompat import androidx.core.widget.TextViewCompat
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.matrix.android.api.permalinks.MatrixLinkify
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.features.html.PillImageSpan import im.vector.riotredesign.features.html.PillImageSpan
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import me.saket.bettermovementmethod.BetterLinkMovementMethod
@EpoxyModelClass(layout = R.layout.item_timeline_event_base) @EpoxyModelClass(layout = R.layout.item_timeline_event_base)
abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() { abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
@ -41,18 +37,29 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
var message: CharSequence? = null var message: CharSequence? = null
@EpoxyAttribute @EpoxyAttribute
override lateinit var informationData: MessageInformationData override lateinit var informationData: MessageInformationData
@EpoxyAttribute
var clickListener: View.OnClickListener? = null val mvmtMethod = BetterLinkMovementMethod.newInstance().also {
it.setOnLinkClickListener { textView, url ->
//Return false to let android manage the click on the link
false
}
it.setOnLinkLongClickListener { textView, url ->
//Long clicks are handled by parent, return true to block android to do something with url
true
}
}
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
MatrixLinkify.addLinkMovementMethod(holder.messageView)
holder.messageView.movementMethod = mvmtMethod
val textFuture = PrecomputedTextCompat.getTextFuture(message ?: "", val textFuture = PrecomputedTextCompat.getTextFuture(message ?: "",
TextViewCompat.getTextMetricsParams(holder.messageView), TextViewCompat.getTextMetricsParams(holder.messageView),
null) null)
holder.messageView.setTextFuture(textFuture) holder.messageView.setTextFuture(textFuture)
holder.messageView.renderSendState() holder.messageView.renderSendState()
holder.messageView.setOnClickListener (clickListener) holder.messageView.setOnClickListener(cellClickListener)
holder.messageView.setOnLongClickListener(longClickListener) holder.messageView.setOnLongClickListener(longClickListener)
findPillsAndProcess { it.bind(holder.messageView) } findPillsAndProcess { it.bind(holder.messageView) }
} }

View File

@ -19,6 +19,8 @@
package im.vector.riotredesign.features.html package im.vector.riotredesign.features.html
import android.content.Context import android.content.Context
import android.text.style.ClickableSpan
import android.text.style.URLSpan
import im.vector.matrix.android.api.permalinks.PermalinkData import im.vector.matrix.android.api.permalinks.PermalinkData
import im.vector.matrix.android.api.permalinks.PermalinkParser import im.vector.matrix.android.api.permalinks.PermalinkParser
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
@ -82,6 +84,9 @@ private class MatrixPlugin private constructor(private val glideRequests: GlideR
.setHandler( .setHandler(
"blockquote", "blockquote",
BlockquoteHandler()) BlockquoteHandler())
.setHandler(
"font",
FontTagHandler())
.setHandler( .setHandler(
"sub", "sub",
SubScriptHandler()) SubScriptHandler())
@ -156,6 +161,13 @@ private class MxLinkHandler(private val glideRequests: GlideRequests,
tag.start(), tag.start(),
tag.end() 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)
} }

View File

@ -0,0 +1,60 @@
/*
* 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.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
/**
* custom to matrix for IRC-style font coloring
*/
class FontTagHandler : SimpleTagHandler() {
override fun getSpans(configuration: MarkwonConfiguration, renderProps: RenderProps, tag: HtmlTag): Any? {
val colorString = tag.attributes()["color"]?.let { parseColor(it) } ?: Color.BLACK
return ForegroundColorSpan(colorString)
}
private fun parseColor(color_name: String): Int {
try {
return Color.parseColor(color_name)
} catch (e: Exception) {
//try other w3c colors?
return when (color_name) {
"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
}
}
}
}

View File

@ -73,6 +73,7 @@
<ViewStub <ViewStub
android:id="@+id/messageContentMediaStub" android:id="@+id/messageContentMediaStub"
style="@style/TimelineContentStubLayoutParams" style="@style/TimelineContentStubLayoutParams"
android:inflatedId="@+id/messageContentMedia"
android:layout="@layout/item_timeline_event_media_message_stub" android:layout="@layout/item_timeline_event_media_message_stub"
tools:ignore="MissingConstraints" /> tools:ignore="MissingConstraints" />