From 6d7f2670df15ef32baa00497f122b46f2945273e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 3 Dec 2019 16:02:07 +0100 Subject: [PATCH] Make url clickable on the preview of event in the bottom sheet --- .../BottomSheetItemMessagePreview.kt | 5 +++ .../home/room/detail/RoomDetailFragment.kt | 6 +++ .../timeline/action/EventSharedAction.kt | 8 ++++ .../action/MessageActionsBottomSheet.kt | 12 ++++++ .../action/MessageActionsEpoxyController.kt | 7 +++- .../timeline/factory/MessageItemFactory.kt | 34 +++++---------- .../detail/timeline/item/MessageTextItem.kt | 25 +---------- .../timeline/tools/EventRenderingTools.kt | 42 +++++++++++++++++++ vector/src/main/res/values/styles_riot.xml | 3 ++ 9 files changed, 93 insertions(+), 49 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemMessagePreview.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemMessagePreview.kt index 3e3f1d3cf2..7f6ff6fc78 100644 --- a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemMessagePreview.kt +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemMessagePreview.kt @@ -25,6 +25,8 @@ import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.extensions.setTextOrHide import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController +import im.vector.riotx.features.home.room.detail.timeline.tools.createLinkMovementMethod import im.vector.riotx.features.home.room.detail.timeline.tools.findPillsAndProcess /** @@ -45,10 +47,13 @@ abstract class BottomSheetItemMessagePreview : VectorEpoxyModel { roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(action.senderId)) } + is EventSharedAction.OnUrlClicked -> { + onUrlClicked(action.url) + } + is EventSharedAction.OnUrlLongClicked -> { + onUrlLongClicked(action.url) + } else -> { Toast.makeText(context, "Action $action is not implemented yet", Toast.LENGTH_LONG).show() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/EventSharedAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/EventSharedAction.kt index 37d96ad62c..8077786d06 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/EventSharedAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/EventSharedAction.kt @@ -88,4 +88,12 @@ sealed class EventSharedAction(@StringRes val titleRes: Int, @DrawableRes val ic data class ViewEditHistory(val messageInformationData: MessageInformationData) : EventSharedAction(R.string.message_view_edit_history, R.drawable.ic_view_edit_history) + + // An url in the event preview has been clicked + data class OnUrlClicked(val url: String) : + EventSharedAction(0, 0) + + // An url in the event preview has been long clicked + data class OnUrlLongClicked(val url: String) : + EventSharedAction(0, 0) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt index 3f4171f733..a5bf6f8558 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt @@ -68,6 +68,18 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), Message messageActionsEpoxyController.listener = this } + override fun onUrlClicked(url: String): Boolean { + sharedActionViewModel.post(EventSharedAction.OnUrlClicked(url)) + // Always consume + return true + } + + override fun onUrlLongClicked(url: String): Boolean { + sharedActionViewModel.post(EventSharedAction.OnUrlLongClicked(url)) + // Always consume + return true + } + override fun didSelectMenuAction(eventAction: EventSharedAction) { if (eventAction is EventSharedAction.ReportContent) { // Toggle report menu diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt index b561a6df3c..53a7ce0354 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt @@ -23,6 +23,8 @@ import im.vector.riotx.R import im.vector.riotx.core.epoxy.bottomsheet.* import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController +import im.vector.riotx.features.home.room.detail.timeline.tools.linkify import javax.inject.Inject /** @@ -44,7 +46,8 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid avatarUrl(state.informationData.avatarUrl ?: "") senderId(state.informationData.senderId) senderName(state.senderName()) - body(body) + urlClickCallback(listener) + body(body.linkify(listener)) time(state.time()) } } @@ -127,7 +130,7 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid } } - interface MessageActionsEpoxyControllerListener { + interface MessageActionsEpoxyControllerListener : TimelineEventController.UrlClickCallback { fun didSelectMenuAction(eventAction: EventSharedAction) } } 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 ac6c563099..417f8d2f9a 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 @@ -24,8 +24,6 @@ import android.text.style.ClickableSpan import android.text.style.ForegroundColorSpan import android.view.View import dagger.Lazy -import im.vector.matrix.android.api.permalinks.MatrixLinkify -import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan import im.vector.matrix.android.api.session.events.model.RelationType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.message.* @@ -35,7 +33,6 @@ import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyModel -import im.vector.riotx.core.linkify.VectorLinkify import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.utils.DebouncedClickListener @@ -45,8 +42,9 @@ import im.vector.riotx.core.utils.isLocalFile import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController 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.home.room.detail.timeline.tools.linkify import im.vector.riotx.features.html.CodeVisitor +import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.media.ImageContentRenderer import im.vector.riotx.features.media.VideoContentRenderer import me.gujun.android.span.span @@ -89,7 +87,7 @@ class MessageItemFactory @Inject constructor( return defaultItemFactory.create(malformedText, informationData, highlight, callback) } if (messageContent.relatesTo?.type == RelationType.REPLACE - || event.isEncrypted() && event.root.content.toModel()?.relatesTo?.type == RelationType.REPLACE + || event.isEncrypted() && event.root.content.toModel()?.relatesTo?.type == RelationType.REPLACE ) { // This is an edit event, we should it when debugging as a notice event return noticeItemFactory.create(event, highlight, readMarkerVisible, callback) @@ -195,8 +193,7 @@ class MessageItemFactory @Inject constructor( val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() val thumbnailData = ImageContentRenderer.Data( filename = messageContent.body, - url = messageContent.videoInfo?.thumbnailFile?.url - ?: messageContent.videoInfo?.thumbnailUrl, + url = messageContent.videoInfo?.thumbnailFile?.url ?: messageContent.videoInfo?.thumbnailUrl, elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(), height = messageContent.videoInfo?.height, maxHeight = maxHeight, @@ -258,7 +255,7 @@ class MessageItemFactory @Inject constructor( highlight: Boolean, callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes): MessageTextItem? { - val linkifiedBody = linkifyBody(body, callback) + val linkifiedBody = body.linkify(callback) return MessageTextItem_().apply { if (informationData.hasBeenEdited) { @@ -326,9 +323,9 @@ class MessageItemFactory @Inject constructor( // nop } }, - editStart, - editEnd, - Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + editStart, + editEnd, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE) return spannable } @@ -344,7 +341,7 @@ class MessageItemFactory @Inject constructor( textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) textStyle = "italic" } - linkifyBody(formattedBody, callback) + formattedBody.linkify(callback) } return MessageTextItem_() .leftGuideline(avatarSizeProvider.leftGuideline) @@ -361,7 +358,7 @@ class MessageItemFactory @Inject constructor( attributes: AbsMessageItem.Attributes): MessageTextItem? { val message = messageContent.body.let { val formattedBody = "* ${informationData.memberName} $it" - linkifyBody(formattedBody, callback) + formattedBody.linkify(callback) } return MessageTextItem_() .apply { @@ -386,17 +383,6 @@ class MessageItemFactory @Inject constructor( .highlighted(highlight) } - private fun linkifyBody(body: CharSequence, callback: TimelineEventController.Callback?): CharSequence { - val spannable = SpannableStringBuilder(body) - MatrixLinkify.addLinks(spannable, object : MatrixPermalinkSpan.Callback { - override fun onUrlClicked(url: String) { - callback?.onUrlClicked(url) - } - }) - VectorLinkify.addLinks(spannable, true) - return spannable - } - companion object { private const val MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT = 5 } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageTextItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageTextItem.kt index cbdc192425..15aa5aa4cb 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageTextItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageTextItem.kt @@ -16,17 +16,15 @@ package im.vector.riotx.features.home.room.detail.timeline.item -import android.view.MotionEvent import androidx.appcompat.widget.AppCompatTextView import androidx.core.text.PrecomputedTextCompat import androidx.core.widget.TextViewCompat import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R -import im.vector.riotx.core.utils.isValidUrl import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController +import im.vector.riotx.features.home.room.detail.timeline.tools.createLinkMovementMethod import im.vector.riotx.features.home.room.detail.timeline.tools.findPillsAndProcess -import me.saket.bettermovementmethod.BetterLinkMovementMethod @EpoxyModelClass(layout = R.layout.item_timeline_event_base) abstract class MessageTextItem : AbsMessageItem() { @@ -40,28 +38,9 @@ abstract class MessageTextItem : AbsMessageItem() { @EpoxyAttribute var urlClickCallback: TimelineEventController.UrlClickCallback? = null - // Better link movement methods fixes the issue when - // long pressing to open the context menu on a TextView also triggers an autoLink click. - private val mvmtMethod = BetterLinkMovementMethod.newInstance().also { - it.setOnLinkClickListener { _, url -> - // Return false to let android manage the click on the link, or true if the link is handled by the application - url.isValidUrl() && urlClickCallback?.onUrlClicked(url) == true - } - // We need also to fix the case when long click on link will trigger long click on cell - it.setOnLinkLongClickListener { tv, url -> - // Long clicks are handled by parent, return true to block android to do something with url - if (url.isValidUrl() && urlClickCallback?.onUrlLongClicked(url) == true) { - tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0)) - true - } else { - false - } - } - } - override fun bind(holder: Holder) { super.bind(holder) - holder.messageView.movementMethod = mvmtMethod + holder.messageView.movementMethod = createLinkMovementMethod(urlClickCallback) if (useBigFont) { holder.messageView.textSize = 44F } else { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/tools/EventRenderingTools.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/tools/EventRenderingTools.kt index 685799cd32..492248985e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/tools/EventRenderingTools.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/tools/EventRenderingTools.kt @@ -16,12 +16,20 @@ package im.vector.riotx.features.home.room.detail.timeline.tools +import android.text.SpannableStringBuilder +import android.view.MotionEvent import androidx.core.text.toSpannable +import im.vector.matrix.android.api.permalinks.MatrixLinkify +import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan +import im.vector.riotx.core.linkify.VectorLinkify +import im.vector.riotx.core.utils.isValidUrl +import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.html.PillImageSpan import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import me.saket.bettermovementmethod.BetterLinkMovementMethod fun CharSequence.findPillsAndProcess(processBlock: (PillImageSpan) -> Unit) { GlobalScope.launch(Dispatchers.Main) { @@ -32,3 +40,37 @@ fun CharSequence.findPillsAndProcess(processBlock: (PillImageSpan) -> Unit) { }.forEach { processBlock(it) } } } + +fun CharSequence.linkify(callback: TimelineEventController.UrlClickCallback?): CharSequence { + val spannable = SpannableStringBuilder(this) + MatrixLinkify.addLinks(spannable, object : MatrixPermalinkSpan.Callback { + override fun onUrlClicked(url: String) { + callback?.onUrlClicked(url) + } + }) + VectorLinkify.addLinks(spannable, true) + return spannable +} + +// Better link movement methods fixes the issue when +// long pressing to open the context menu on a TextView also triggers an autoLink click. +fun createLinkMovementMethod(urlClickCallback: TimelineEventController.UrlClickCallback?): BetterLinkMovementMethod { + return BetterLinkMovementMethod.newInstance() + .apply { + setOnLinkClickListener { _, url -> + // Return false to let android manage the click on the link, or true if the link is handled by the application + url.isValidUrl() && urlClickCallback?.onUrlClicked(url) == true + } + + // We need also to fix the case when long click on link will trigger long click on cell + setOnLinkLongClickListener { tv, url -> + // Long clicks are handled by parent, return true to block android to do something with url + if (url.isValidUrl() && urlClickCallback?.onUrlLongClicked(url) == true) { + tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0)) + true + } else { + false + } + } + } +} diff --git a/vector/src/main/res/values/styles_riot.xml b/vector/src/main/res/values/styles_riot.xml index 798c7ced87..ea41a3c7ca 100644 --- a/vector/src/main/res/values/styles_riot.xml +++ b/vector/src/main/res/values/styles_riot.xml @@ -266,6 +266,7 @@ @color/riot_secondary_text_color_dark @color/riot_tertiary_text_color_dark + @color/riotx_links