Merge pull request #731 from vector-im/feature/fix_pills
Fix issue with pill: also send the text after the last pills
This commit is contained in:
commit
c495aa4914
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ class SasEmojiController : TypedEpoxyController<SasState>() {
|
|||
if (data == null) return
|
||||
|
||||
data.emojiList.forEachIndexed { idx, emojiRepresentation ->
|
||||
itemSasEmoji {
|
||||
sasEmojiItem {
|
||||
id(idx)
|
||||
index(idx)
|
||||
emojiRepresentation(emojiRepresentation)
|
||||
|
|
|
@ -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<ItemSasEmoji.Holder>() {
|
||||
abstract class SasEmojiItem : VectorEpoxyModel<SasEmojiItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute
|
||||
var index: Int = 0
|
|
@ -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<BottomSheetItemAction.Holder>() {
|
||||
abstract class BottomSheetActionItem : VectorEpoxyModel<BottomSheetActionItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute
|
||||
@DrawableRes
|
|
@ -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<BottomSheetItemMessagePreview.Holder>() {
|
||||
abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel<BottomSheetMessagePreviewItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var avatarRenderer: AvatarRenderer
|
||||
|
@ -44,11 +46,15 @@ abstract class BottomSheetItemMessagePreview : VectorEpoxyModel<BottomSheetItemM
|
|||
lateinit var body: CharSequence
|
||||
@EpoxyAttribute
|
||||
var time: CharSequence? = null
|
||||
@EpoxyAttribute
|
||||
var movementMethod: MovementMethod? = null
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
avatarRenderer.render(avatarUrl, senderId, senderName, holder.avatar)
|
||||
holder.sender.setTextOrHide(senderName)
|
||||
holder.body.movementMethod = movementMethod
|
||||
holder.body.text = body
|
||||
body.findPillsAndProcess { it.bind(holder.body) }
|
||||
holder.timestamp.setTextOrHide(time)
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
|||
* A quick reaction list for bottom sheet.
|
||||
*/
|
||||
@EpoxyModelClass(layout = R.layout.item_bottom_sheet_quick_reaction)
|
||||
abstract class BottomSheetItemQuickReactions : VectorEpoxyModel<BottomSheetItemQuickReactions.Holder>() {
|
||||
abstract class BottomSheetQuickReactionsItem : VectorEpoxyModel<BottomSheetQuickReactionsItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var fontProvider: EmojiCompatFontProvider
|
|
@ -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<BottomSheetItemRoomPreview.Holder>() {
|
||||
abstract class BottomSheetRoomPreviewItem : VectorEpoxyModel<BottomSheetRoomPreviewItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var avatarRenderer: AvatarRenderer
|
|
@ -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<BottomSheetItemSendState.Holder>() {
|
||||
abstract class BottomSheetSendStateItem : VectorEpoxyModel<BottomSheetSendStateItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute
|
||||
var showProgress: Boolean = false
|
|
@ -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<BottomSheetItemSeparator.Holder>() {
|
||||
abstract class BottomSheetSeparatorItem : VectorEpoxyModel<BottomSheetSeparatorItem.Holder>() {
|
||||
|
||||
class Holder : VectorEpoxyHolder()
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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<EncryptedEventContent>()?.relatesTo?.type == RelationType.REPLACE
|
||||
|| event.isEncrypted() && event.root.content.toModel<EncryptedEventContent>()?.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
|
||||
}
|
||||
|
|
|
@ -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<MessageTextItem.Holder>() {
|
||||
|
@ -43,30 +35,11 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
|||
@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<MessageTextItem.Holder>() {
|
|||
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<MessageTextItem.Holder>() {
|
|||
holder.messageView.setTextFuture(textFuture)
|
||||
}
|
||||
|
||||
private fun findPillsAndProcess(processBlock: (span: PillImageSpan) -> Unit) {
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
val pillImageSpans: Array<PillImageSpan>? = 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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -266,6 +266,7 @@
|
|||
<item name="android:textColorSecondary">@color/riot_secondary_text_color_dark</item>
|
||||
<!-- Default color for text View -->
|
||||
<item name="android:textColorTertiary">@color/riot_tertiary_text_color_dark</item>
|
||||
<item name="android:textColorLink">@color/riotx_links</item>
|
||||
</style>
|
||||
|
||||
<style name="Vector.BottomSheet.Light" parent="Theme.Design.Light.BottomSheetDialog">
|
||||
|
@ -273,6 +274,7 @@
|
|||
<item name="android:textColorSecondary">@color/riot_secondary_text_color_light</item>
|
||||
<!-- Default color for text View -->
|
||||
<item name="android:textColorTertiary">@color/riot_tertiary_text_color_light</item>
|
||||
<item name="android:textColorLink">@color/riotx_links</item>
|
||||
</style>
|
||||
|
||||
<style name="Vector.BottomSheet.Status" parent="Theme.Design.Light.BottomSheetDialog">
|
||||
|
@ -280,6 +282,7 @@
|
|||
<item name="android:textColorSecondary">@color/riot_secondary_text_color_status</item>
|
||||
<!-- Default color for text View -->
|
||||
<item name="android:textColorTertiary">@color/riot_tertiary_text_color_status</item>
|
||||
<item name="android:textColorLink">@color/link_color_status</item>
|
||||
</style>
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue