diff --git a/CHANGES.md b/CHANGES.md index 64de4ac68d..3ead09faac 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,7 @@ Features ✨: Improvements 🙌: - Send mention Pills from composer + - Links in message preview in the bottom sheet are now active. Other changes: - Fix a small grammatical error when an empty room list is shown. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt index 02f48e5800..580e49b2ce 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt @@ -62,10 +62,14 @@ internal class TextPillsUtils @Inject constructor( var currIndex = 0 pills.forEachIndexed { _, (urlSpan, start, end) -> // We want to replace with the pill with a html link + // append text before pill append(text, currIndex, start) + // append the pill append(String.format(template, urlSpan.userId, urlSpan.displayName)) currIndex = end } + // append text after the last pill + append(text, currIndex, text.length) } } diff --git a/vector/src/debug/java/im/vector/riotx/features/debug/sas/SasEmojiController.kt b/vector/src/debug/java/im/vector/riotx/features/debug/sas/SasEmojiController.kt index daf432fb45..6804828b20 100644 --- a/vector/src/debug/java/im/vector/riotx/features/debug/sas/SasEmojiController.kt +++ b/vector/src/debug/java/im/vector/riotx/features/debug/sas/SasEmojiController.kt @@ -29,7 +29,7 @@ class SasEmojiController : TypedEpoxyController() { if (data == null) return data.emojiList.forEachIndexed { idx, emojiRepresentation -> - itemSasEmoji { + sasEmojiItem { id(idx) index(idx) emojiRepresentation(emojiRepresentation) diff --git a/vector/src/debug/java/im/vector/riotx/features/debug/sas/ItemSasEmoji.kt b/vector/src/debug/java/im/vector/riotx/features/debug/sas/SasEmojiItem.kt similarity index 96% rename from vector/src/debug/java/im/vector/riotx/features/debug/sas/ItemSasEmoji.kt rename to vector/src/debug/java/im/vector/riotx/features/debug/sas/SasEmojiItem.kt index 92d9bc0b11..cf35873f6b 100644 --- a/vector/src/debug/java/im/vector/riotx/features/debug/sas/ItemSasEmoji.kt +++ b/vector/src/debug/java/im/vector/riotx/features/debug/sas/SasEmojiItem.kt @@ -25,7 +25,7 @@ import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel @EpoxyModelClass(layout = im.vector.riotx.R.layout.item_sas_emoji) -abstract class ItemSasEmoji : VectorEpoxyModel() { +abstract class SasEmojiItem : VectorEpoxyModel() { @EpoxyAttribute var index: Int = 0 diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemAction.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetActionItem.kt similarity index 97% rename from vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemAction.kt rename to vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetActionItem.kt index 483650a434..c55dbdde8a 100644 --- a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemAction.kt +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetActionItem.kt @@ -37,7 +37,7 @@ import im.vector.riotx.features.themes.ThemeUtils * A action for bottom sheet. */ @EpoxyModelClass(layout = R.layout.item_bottom_sheet_action) -abstract class BottomSheetItemAction : VectorEpoxyModel() { +abstract class BottomSheetActionItem : VectorEpoxyModel() { @EpoxyAttribute @DrawableRes 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/BottomSheetMessagePreviewItem.kt similarity index 83% rename from vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemMessagePreview.kt rename to vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt index 999068b289..8105d7a7c0 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/BottomSheetMessagePreviewItem.kt @@ -16,6 +16,7 @@ */ package im.vector.riotx.core.epoxy.bottomsheet +import android.text.method.MovementMethod import android.widget.ImageView import android.widget.TextView import com.airbnb.epoxy.EpoxyAttribute @@ -25,12 +26,13 @@ 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.tools.findPillsAndProcess /** * A message preview for bottom sheet. */ @EpoxyModelClass(layout = R.layout.item_bottom_sheet_message_preview) -abstract class BottomSheetItemMessagePreview : VectorEpoxyModel() { +abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel() { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @@ -44,11 +46,15 @@ abstract class BottomSheetItemMessagePreview : VectorEpoxyModel() { +abstract class BottomSheetQuickReactionsItem : VectorEpoxyModel() { @EpoxyAttribute lateinit var fontProvider: EmojiCompatFontProvider diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemRoomPreview.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetRoomPreviewItem.kt similarity index 95% rename from vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemRoomPreview.kt rename to vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetRoomPreviewItem.kt index 9b9d0fc380..1a5b4e2f66 100644 --- a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemRoomPreview.kt +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetRoomPreviewItem.kt @@ -31,7 +31,7 @@ import im.vector.riotx.features.home.AvatarRenderer * A room preview for bottom sheet. */ @EpoxyModelClass(layout = R.layout.item_bottom_sheet_room_preview) -abstract class BottomSheetItemRoomPreview : VectorEpoxyModel() { +abstract class BottomSheetRoomPreviewItem : VectorEpoxyModel() { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemSendState.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetSendStateItem.kt similarity index 94% rename from vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemSendState.kt rename to vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetSendStateItem.kt index 08d727cfa9..8f830ba706 100644 --- a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemSendState.kt +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetSendStateItem.kt @@ -30,7 +30,7 @@ import im.vector.riotx.core.epoxy.VectorEpoxyModel * A send state for bottom sheet. */ @EpoxyModelClass(layout = R.layout.item_bottom_sheet_message_status) -abstract class BottomSheetItemSendState : VectorEpoxyModel() { +abstract class BottomSheetSendStateItem : VectorEpoxyModel() { @EpoxyAttribute var showProgress: Boolean = false diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemSeparator.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetSeparatorItem.kt similarity index 90% rename from vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemSeparator.kt rename to vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetSeparatorItem.kt index fddf507bf9..dd41d5dd66 100644 --- a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemSeparator.kt +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetSeparatorItem.kt @@ -22,7 +22,7 @@ import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel @EpoxyModelClass(layout = R.layout.item_bottom_sheet_divider) -abstract class BottomSheetItemSeparator : VectorEpoxyModel() { +abstract class BottomSheetSeparatorItem : VectorEpoxyModel() { class Holder : VectorEpoxyHolder() } 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 59437273d6..31278a1fff 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 @@ -1163,6 +1163,12 @@ class RoomDetailFragment @Inject constructor( is EventSharedAction.IgnoreUser -> { 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..efbfd3434c 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,9 @@ 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.createLinkMovementMethod +import im.vector.riotx.features.home.room.detail.timeline.tools.linkify import javax.inject.Inject /** @@ -38,26 +41,27 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid // Message preview val body = state.messageBody if (body != null) { - bottomSheetItemMessagePreview { + bottomSheetMessagePreviewItem { id("preview") avatarRenderer(avatarRenderer) avatarUrl(state.informationData.avatarUrl ?: "") senderId(state.informationData.senderId) senderName(state.senderName()) - body(body) + movementMethod(createLinkMovementMethod(listener)) + body(body.linkify(listener)) time(state.time()) } } // Send state if (state.informationData.sendState.isSending()) { - bottomSheetItemSendState { + bottomSheetSendStateItem { id("send_state") showProgress(true) text(stringProvider.getString(R.string.event_status_sending_message)) } } else if (state.informationData.sendState.hasFailed()) { - bottomSheetItemSendState { + bottomSheetSendStateItem { id("send_state") showProgress(false) text(stringProvider.getString(R.string.unable_to_send_message)) @@ -68,16 +72,16 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid // Quick reactions if (state.canReact() && state.quickStates is Success) { // Separator - bottomSheetItemSeparator { + bottomSheetSeparatorItem { id("reaction_separator") } - bottomSheetItemQuickReactions { + bottomSheetQuickReactionsItem { id("quick_reaction") fontProvider(fontProvider) texts(state.quickStates()?.map { it.reaction }.orEmpty()) selecteds(state.quickStates.invoke().map { it.isSelected }) - listener(object : BottomSheetItemQuickReactions.Listener { + listener(object : BottomSheetQuickReactionsItem.Listener { override fun didSelect(emoji: String, selected: Boolean) { listener?.didSelectMenuAction(EventSharedAction.QuickReact(state.eventId, emoji, selected)) } @@ -86,18 +90,18 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid } // Separator - bottomSheetItemSeparator { + bottomSheetSeparatorItem { id("actions_separator") } // Action state.actions()?.forEachIndexed { index, action -> if (action is EventSharedAction.Separator) { - bottomSheetItemSeparator { + bottomSheetSeparatorItem { id("separator_$index") } } else { - bottomSheetItemAction { + bottomSheetActionItem { id("action_$index") iconRes(action.iconResId) textRes(action.titleRes) @@ -114,7 +118,7 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid EventSharedAction.ReportContentInappropriate(action.eventId, action.senderId), EventSharedAction.ReportContentCustom(action.eventId, action.senderId) ).forEachIndexed { indexReport, actionReport -> - bottomSheetItemAction { + bottomSheetActionItem { id("actionReport_$indexReport") subMenuItem(true) iconRes(actionReport.iconResId) @@ -127,7 +131,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/EncryptedItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt index e67507d7bb..c7aca768dc 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt @@ -25,9 +25,10 @@ import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider -import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem_ import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory import im.vector.riotx.features.home.room.detail.timeline.helper.MessageItemAttributesFactory +import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem_ +import im.vector.riotx.features.home.room.detail.timeline.tools.createLinkMovementMethod import me.gujun.android.span.span import javax.inject.Inject @@ -57,7 +58,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat } val message = stringProvider.getString(R.string.encrypted_message).takeIf { cryptoError == null } - ?: stringProvider.getString(R.string.notice_crypto_unable_to_decrypt, errorDescription) + ?: stringProvider.getString(R.string.notice_crypto_unable_to_decrypt, errorDescription) val spannableStr = span(message) { textStyle = "italic" textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) @@ -72,7 +73,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat .highlighted(highlight) .attributes(attributes) .message(spannableStr) - .urlClickCallback(callback) + .movementMethod(createLinkMovementMethod(callback)) } else -> null } 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..de2686de04 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,10 @@ 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.createLinkMovementMethod +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 +88,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 +194,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 +256,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) { @@ -273,7 +271,7 @@ class MessageItemFactory @Inject constructor( .leftGuideline(avatarSizeProvider.leftGuideline) .attributes(attributes) .highlighted(highlight) - .urlClickCallback(callback) + .movementMethod(createLinkMovementMethod(callback)) } private fun buildCodeBlockItem(formattedBody: CharSequence, @@ -326,9 +324,9 @@ class MessageItemFactory @Inject constructor( // nop } }, - editStart, - editEnd, - Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + editStart, + editEnd, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE) return spannable } @@ -344,14 +342,14 @@ 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) .attributes(attributes) .message(message) .highlighted(highlight) - .urlClickCallback(callback) + .movementMethod(createLinkMovementMethod(callback)) } private fun buildEmoteMessageItem(messageContent: MessageEmoteContent, @@ -361,7 +359,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 { @@ -375,7 +373,7 @@ class MessageItemFactory @Inject constructor( .leftGuideline(avatarSizeProvider.leftGuideline) .attributes(attributes) .highlighted(highlight) - .urlClickCallback(callback) + .movementMethod(createLinkMovementMethod(callback)) } private fun buildRedactedItem(attributes: AbsMessageItem.Attributes, @@ -386,17 +384,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 45a6e2e743..5ee0576be7 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,22 +16,14 @@ package im.vector.riotx.features.home.room.detail.timeline.item -import android.view.MotionEvent +import android.text.method.MovementMethod import androidx.appcompat.widget.AppCompatTextView import androidx.core.text.PrecomputedTextCompat -import androidx.core.text.toSpannable 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.html.PillImageSpan -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import me.saket.bettermovementmethod.BetterLinkMovementMethod +import im.vector.riotx.features.home.room.detail.timeline.tools.findPillsAndProcess @EpoxyModelClass(layout = R.layout.item_timeline_event_base) abstract class MessageTextItem : AbsMessageItem() { @@ -43,30 +35,11 @@ abstract class MessageTextItem : AbsMessageItem() { @EpoxyAttribute var useBigFont: Boolean = false @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 - } - } - } + var movementMethod: MovementMethod? = null override fun bind(holder: Holder) { super.bind(holder) - holder.messageView.movementMethod = mvmtMethod + holder.messageView.movementMethod = movementMethod if (useBigFont) { holder.messageView.textSize = 44F } else { @@ -76,7 +49,7 @@ abstract class MessageTextItem : AbsMessageItem() { holder.messageView.setOnClickListener(attributes.itemClickListener) holder.messageView.setOnLongClickListener(attributes.itemLongClickListener) if (searchForPills) { - findPillsAndProcess { it.bind(holder.messageView) } + message?.findPillsAndProcess { it.bind(holder.messageView) } } val textFuture = PrecomputedTextCompat.getTextFuture( message ?: "", @@ -85,17 +58,6 @@ abstract class MessageTextItem : AbsMessageItem() { holder.messageView.setTextFuture(textFuture) } - private fun findPillsAndProcess(processBlock: (span: PillImageSpan) -> Unit) { - GlobalScope.launch(Dispatchers.Main) { - val pillImageSpans: Array? = withContext(Dispatchers.IO) { - message?.toSpannable()?.let { spannable -> - spannable.getSpans(0, spannable.length, PillImageSpan::class.java) - } - } - pillImageSpans?.forEach { processBlock(it) } - } - } - override fun getViewType() = STUB_ID class Holder : AbsMessageItem.Holder(STUB_ID) { 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 new file mode 100644 index 0000000000..492248985e --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/tools/EventRenderingTools.kt @@ -0,0 +1,76 @@ +/* + * 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.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) { + withContext(Dispatchers.IO) { + toSpannable().let { spannable -> + spannable.getSpans(0, spannable.length, PillImageSpan::class.java) + } + }.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/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt index 2e17464cc6..84fd5bc6f2 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt @@ -18,9 +18,9 @@ package im.vector.riotx.features.home.room.list.actions import android.view.View import com.airbnb.epoxy.TypedEpoxyController import im.vector.matrix.android.api.session.room.notification.RoomNotificationState -import im.vector.riotx.core.epoxy.bottomsheet.bottomSheetItemAction -import im.vector.riotx.core.epoxy.bottomsheet.bottomSheetItemRoomPreview -import im.vector.riotx.core.epoxy.bottomsheet.bottomSheetItemSeparator +import im.vector.riotx.core.epoxy.bottomsheet.bottomSheetActionItem +import im.vector.riotx.core.epoxy.bottomsheet.bottomSheetRoomPreviewItem +import im.vector.riotx.core.epoxy.bottomsheet.bottomSheetSeparatorItem import im.vector.riotx.features.home.AvatarRenderer import javax.inject.Inject @@ -36,7 +36,7 @@ class RoomListQuickActionsEpoxyController @Inject constructor(private val avatar val roomSummary = state.roomSummary() ?: return // Preview - bottomSheetItemRoomPreview { + bottomSheetRoomPreviewItem { id("preview") avatarRenderer(avatarRenderer) roomName(roomSummary.displayName) @@ -46,7 +46,7 @@ class RoomListQuickActionsEpoxyController @Inject constructor(private val avatar } // Notifications - bottomSheetItemSeparator { + bottomSheetSeparatorItem { id("notifications_separator") } @@ -57,7 +57,7 @@ class RoomListQuickActionsEpoxyController @Inject constructor(private val avatar RoomListQuickActionsSharedAction.NotificationsMute(roomSummary.roomId).toBottomSheetItem(3, selectedRoomState) // Leave - bottomSheetItemSeparator { + bottomSheetSeparatorItem { id("leave_separator") } RoomListQuickActionsSharedAction.Leave(roomSummary.roomId).toBottomSheetItem(5) @@ -72,7 +72,7 @@ class RoomListQuickActionsEpoxyController @Inject constructor(private val avatar is RoomListQuickActionsSharedAction.Settings, is RoomListQuickActionsSharedAction.Leave -> false } - return bottomSheetItemAction { + return bottomSheetActionItem { id("action_$index") selected(selected) iconRes(iconResId) 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