Send mention pills from composer

This commit is contained in:
Valere 2019-11-15 00:14:28 +01:00 committed by Benoit Marty
parent bf9ce4f690
commit 6bd7257cf2
13 changed files with 141 additions and 42 deletions

View file

@ -72,7 +72,7 @@ interface RelationService {
*/ */
fun editTextMessage(targetEventId: String, fun editTextMessage(targetEventId: String,
msgType: String, msgType: String,
newBodyText: String, newBodyText: CharSequence,
newBodyAutoMarkdown: Boolean, newBodyAutoMarkdown: Boolean,
compatibilityBodyText: String = "* $newBodyText"): Cancelable compatibilityBodyText: String = "* $newBodyText"): Cancelable
@ -97,12 +97,14 @@ interface RelationService {
/** /**
* Reply to an event in the timeline (must be in same room) * Reply to an event in the timeline (must be in same room)
* https://matrix.org/docs/spec/client_server/r0.4.0.html#id350 * https://matrix.org/docs/spec/client_server/r0.4.0.html#id350
* The replyText can be a Spannable and contains special spans (UserMentionSpan) that will be translated
* by the sdk into pills.
* @param eventReplied the event referenced by the reply * @param eventReplied the event referenced by the reply
* @param replyText the reply text * @param replyText the reply text
* @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present * @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
*/ */
fun replyToMessage(eventReplied: TimelineEvent, fun replyToMessage(eventReplied: TimelineEvent,
replyText: String, replyText: CharSequence,
autoMarkdown: Boolean = false): Cancelable? autoMarkdown: Boolean = false): Cancelable?
fun getEventSummaryLive(eventId: String): LiveData<Optional<EventAnnotationsSummary>> fun getEventSummaryLive(eventId: String): LiveData<Optional<EventAnnotationsSummary>>

View file

@ -29,12 +29,14 @@ interface SendService {
/** /**
* Method to send a text message asynchronously. * Method to send a text message asynchronously.
* The text to send can be a Spannable and contains special spans (UserMentionSpan) that will be translated
* by the sdk into pills.
* @param text the text message to send * @param text the text message to send
* @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE * @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE
* @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present * @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
* @return a [Cancelable] * @return a [Cancelable]
*/ */
fun sendTextMessage(text: String, msgType: String = MessageType.MSGTYPE_TEXT, autoMarkdown: Boolean = false): Cancelable fun sendTextMessage(text: CharSequence, msgType: String = MessageType.MSGTYPE_TEXT, autoMarkdown: Boolean = false): Cancelable
/** /**
* Method to send a text message with a formatted body. * Method to send a text message with a formatted body.
@ -42,7 +44,7 @@ interface SendService {
* @param formattedText The formatted body using MessageType#FORMAT_MATRIX_HTML * @param formattedText The formatted body using MessageType#FORMAT_MATRIX_HTML
* @return a [Cancelable] * @return a [Cancelable]
*/ */
fun sendFormattedTextMessage(text: String, formattedText: String): Cancelable fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String = MessageType.MSGTYPE_TEXT): Cancelable
/** /**
* Method to send a media asynchronously. * Method to send a media asynchronously.

View file

@ -0,0 +1,25 @@
/*
* 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.matrix.android.api.session.room.send
/**
* Tag class for spans that should mention a user.
* These Spans will be transformed into pills when detected in message to send
*/
interface UserMentionSpan {
abstract val displayName: String
abstract val userId: String
}

View file

@ -115,7 +115,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
override fun editTextMessage(targetEventId: String, override fun editTextMessage(targetEventId: String,
msgType: String, msgType: String,
newBodyText: String, newBodyText: CharSequence,
newBodyAutoMarkdown: Boolean, newBodyAutoMarkdown: Boolean,
compatibilityBodyText: String): Cancelable { compatibilityBodyText: String): Cancelable {
val event = eventFactory val event = eventFactory
@ -164,7 +164,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
override fun replyToMessage(eventReplied: TimelineEvent, replyText: String, autoMarkdown: Boolean): Cancelable? { override fun replyToMessage(eventReplied: TimelineEvent, replyText: CharSequence, autoMarkdown: Boolean): Cancelable? {
val event = eventFactory.createReplyTextEvent(roomId, eventReplied, replyText, autoMarkdown) val event = eventFactory.createReplyTextEvent(roomId, eventReplied, replyText, autoMarkdown)
?.also { saveLocalEcho(it) } ?.also { saveLocalEcho(it) }
?: return null ?: return null

View file

@ -68,7 +68,7 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private
private val workerFutureListenerExecutor = Executors.newSingleThreadExecutor() private val workerFutureListenerExecutor = Executors.newSingleThreadExecutor()
override fun sendTextMessage(text: String, msgType: String, autoMarkdown: Boolean): Cancelable { override fun sendTextMessage(text: CharSequence, msgType: String, autoMarkdown: Boolean): Cancelable {
val event = localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown).also { val event = localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown).also {
saveLocalEcho(it) saveLocalEcho(it)
} }
@ -76,8 +76,8 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private
return sendEvent(event) return sendEvent(event)
} }
override fun sendFormattedTextMessage(text: String, formattedText: String): Cancelable { override fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String): Cancelable {
val event = localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText)).also { val event = localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType).also {
saveLocalEcho(it) saveLocalEcho(it)
} }

View file

@ -17,6 +17,7 @@
package im.vector.matrix.android.internal.session.room.send package im.vector.matrix.android.internal.session.room.send
import android.media.MediaMetadataRetriever import android.media.MediaMetadataRetriever
import android.text.SpannableString
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.R import im.vector.matrix.android.R
@ -28,6 +29,7 @@ import im.vector.matrix.android.api.session.room.model.relation.ReactionContent
import im.vector.matrix.android.api.session.room.model.relation.ReactionInfo import im.vector.matrix.android.api.session.room.model.relation.ReactionInfo
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent
import im.vector.matrix.android.api.session.room.send.UserMentionSpan
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.internal.database.helper.addSendingEvent import im.vector.matrix.android.internal.database.helper.addSendingEvent
@ -58,37 +60,67 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use
// TODO Inject // TODO Inject
private val renderer = HtmlRenderer.builder().build() private val renderer = HtmlRenderer.builder().build()
fun createTextEvent(roomId: String, msgType: String, text: String, autoMarkdown: Boolean): Event { fun createTextEvent(roomId: String, msgType: String, text: CharSequence, autoMarkdown: Boolean): Event {
if (msgType == MessageType.MSGTYPE_TEXT) { if (msgType == MessageType.MSGTYPE_TEXT || msgType == MessageType.MSGTYPE_EMOTE) {
return createFormattedTextEvent(roomId, createTextContent(text, autoMarkdown)) return createFormattedTextEvent(roomId, createTextContent(text, autoMarkdown), msgType)
} }
val content = MessageTextContent(type = msgType, body = text) val content = MessageTextContent(type = msgType, body = text.toString())
return createEvent(roomId, content) return createEvent(roomId, content)
} }
private fun createTextContent(text: String, autoMarkdown: Boolean): TextContent { private fun createTextContent(text: CharSequence, autoMarkdown: Boolean): TextContent {
if (autoMarkdown) { if (autoMarkdown) {
val document = parser.parse(text) val source = transformPills(text,"[%2\$s](https://matrix.to/#/%1\$s)") ?: text.toString()
val document = parser.parse(source)
val htmlText = renderer.render(document) val htmlText = renderer.render(document)
if (isFormattedTextPertinent(text, htmlText)) { if (isFormattedTextPertinent(source, htmlText)) {
return TextContent(text, htmlText) return TextContent(source, htmlText)
}
} else {
//Try to detect pills
transformPills(text, "<a href=\"https://matrix.to/#/%1\$s\">%2\$s</a>")?.let {
return TextContent(text.toString(),it)
} }
} }
return TextContent(text) return TextContent(text.toString())
}
private fun transformPills(text: CharSequence,
template : String)
: String? {
val bufSB = StringBuffer()
var currIndex = 0
SpannableString.valueOf(text).let {
val pills = it.getSpans(0, text.length, UserMentionSpan::class.java)
if (pills.isNotEmpty()) {
pills.forEachIndexed { _, urlSpan ->
val start = it.getSpanStart(urlSpan)
val end = it.getSpanEnd(urlSpan)
//We want to replace with the pill with a html link
bufSB.append(text, currIndex, start)
bufSB.append(String.format(template,urlSpan.userId,urlSpan.displayName))
currIndex = end
}
bufSB.append(text, currIndex, text.length)
return bufSB.toString()
} else {
return null
}
}
} }
private fun isFormattedTextPertinent(text: String, htmlText: String?) = private fun isFormattedTextPertinent(text: String, htmlText: String?) =
text != htmlText && htmlText != "<p>${text.trim()}</p>\n" text != htmlText && htmlText != "<p>${text.trim()}</p>\n"
fun createFormattedTextEvent(roomId: String, textContent: TextContent): Event { fun createFormattedTextEvent(roomId: String, textContent: TextContent, msgType: String): Event {
return createEvent(roomId, textContent.toMessageTextContent()) return createEvent(roomId, textContent.toMessageTextContent(msgType))
} }
fun createReplaceTextEvent(roomId: String, fun createReplaceTextEvent(roomId: String,
targetEventId: String, targetEventId: String,
newBodyText: String, newBodyText: CharSequence,
newBodyAutoMarkdown: Boolean, newBodyAutoMarkdown: Boolean,
msgType: String, msgType: String,
compatibilityText: String): Event { compatibilityText: String): Event {
@ -279,7 +311,7 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use
return System.currentTimeMillis() return System.currentTimeMillis()
} }
fun createReplyTextEvent(roomId: String, eventReplied: TimelineEvent, replyText: String, autoMarkdown: Boolean): Event? { fun createReplyTextEvent(roomId: String, eventReplied: TimelineEvent, replyText: CharSequence, autoMarkdown: Boolean): Event? {
// Fallbacks and event representation // Fallbacks and event representation
// TODO Add error/warning logs when any of this is null // TODO Add error/warning logs when any of this is null
val permalink = PermalinkFactory.createPermalink(eventReplied.root) ?: return null val permalink = PermalinkFactory.createPermalink(eventReplied.root) ?: return null
@ -298,7 +330,7 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use
// //
// > <@alice:example.org> This is the original body // > <@alice:example.org> This is the original body
// //
val replyFallback = buildReplyFallback(body, userId, replyText) val replyFallback = buildReplyFallback(body, userId, replyText.toString())
val eventId = eventReplied.root.eventId ?: return null val eventId = eventReplied.root.eventId ?: return null
val content = MessageTextContent( val content = MessageTextContent(

View file

@ -27,7 +27,7 @@ object CommandParser {
* @param textMessage the text message * @param textMessage the text message
* @return a parsed slash command (ok or error) * @return a parsed slash command (ok or error)
*/ */
fun parseSplashCommand(textMessage: String): ParsedCommand { fun parseSplashCommand(textMessage: CharSequence): ParsedCommand {
// check if it has the Slash marker // check if it has the Slash marker
if (!textMessage.startsWith("/")) { if (!textMessage.startsWith("/")) {
return ParsedCommand.ErrorNotACommand return ParsedCommand.ErrorNotACommand
@ -76,7 +76,7 @@ object CommandParser {
} }
} }
Command.EMOTE.command -> { Command.EMOTE.command -> {
val message = textMessage.substring(Command.EMOTE.command.length).trim() val message = textMessage.subSequence(Command.EMOTE.command.length, textMessage.length).trim()
ParsedCommand.SendEmote(message) ParsedCommand.SendEmote(message)
} }

View file

@ -33,7 +33,7 @@ sealed class ParsedCommand {
// Valid commands: // Valid commands:
class SendEmote(val message: String) : ParsedCommand() class SendEmote(val message: CharSequence) : ParsedCommand()
class BanUser(val userId: String, val reason: String) : ParsedCommand() class BanUser(val userId: String, val reason: String) : ParsedCommand()
class UnbanUser(val userId: String) : ParsedCommand() class UnbanUser(val userId: String) : ParsedCommand()
class SetUserPowerLevel(val userId: String, val powerLevel: Int) : ParsedCommand() class SetUserPowerLevel(val userId: String, val powerLevel: Int) : ParsedCommand()

View file

@ -25,7 +25,7 @@ import im.vector.riotx.core.platform.VectorViewModelAction
sealed class RoomDetailAction : VectorViewModelAction { sealed class RoomDetailAction : VectorViewModelAction {
data class SaveDraft(val draft: String) : RoomDetailAction() data class SaveDraft(val draft: String) : RoomDetailAction()
data class SendMessage(val text: String, val autoMarkdown: Boolean) : RoomDetailAction() data class SendMessage(val text: CharSequence, val autoMarkdown: Boolean) : RoomDetailAction()
data class SendMedia(val attachments: List<ContentAttachmentData>) : RoomDetailAction() data class SendMedia(val attachments: List<ContentAttachmentData>) : RoomDetailAction()
data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailAction() data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailAction()
data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailAction() data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailAction()

View file

@ -28,6 +28,7 @@ import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.text.Editable import android.text.Editable
import android.text.Spannable import android.text.Spannable
import android.text.SpannableStringBuilder
import android.view.* import android.view.*
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.TextView import android.widget.TextView
@ -609,7 +610,7 @@ class RoomDetailFragment @Inject constructor(
attachmentTypeSelector.show(composerLayout.attachmentButton, keyboardStateUtils.isKeyboardShowing) attachmentTypeSelector.show(composerLayout.attachmentButton, keyboardStateUtils.isKeyboardShowing)
} }
override fun onSendMessage(text: String) { override fun onSendMessage(text: CharSequence) {
if (lockSendButton) { if (lockSendButton) {
Timber.w("Send button is locked") Timber.w("Send button is locked")
return return
@ -977,7 +978,9 @@ class RoomDetailFragment @Inject constructor(
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onMemberNameClicked(informationData: MessageInformationData) { override fun onMemberNameClicked(informationData: MessageInformationData) {
insertUserDisplayNameInTextEditor(informationData.memberName?.toString()) session.getUser(informationData.senderId)?.let {
insertUserDisplayNameInTextEditor(it)
}
} }
override fun onClickOnReactionPill(informationData: MessageInformationData, reaction: String, on: Boolean) { override fun onClickOnReactionPill(informationData: MessageInformationData, reaction: String, on: Boolean) {
@ -1166,8 +1169,9 @@ class RoomDetailFragment @Inject constructor(
* @param text the text to insert. * @param text the text to insert.
*/ */
// TODO legacy, refactor // TODO legacy, refactor
private fun insertUserDisplayNameInTextEditor(text: String?) { private fun insertUserDisplayNameInTextEditor(member: User) {
// TODO move logic outside of fragment // TODO move logic outside of fragment
val text = member.displayName
if (null != text) { if (null != text) {
// var vibrate = false // var vibrate = false
@ -1176,19 +1180,44 @@ class RoomDetailFragment @Inject constructor(
// current user // current user
if (composerLayout.composerEditText.text.isNullOrBlank()) { if (composerLayout.composerEditText.text.isNullOrBlank()) {
composerLayout.composerEditText.append(Command.EMOTE.command + " ") composerLayout.composerEditText.append(Command.EMOTE.command + " ")
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length ?: 0) composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length
?: 0)
// vibrate = true // vibrate = true
} }
} else { } else {
// another user // another user
val sanitizeDisplayName = sanitizeDisplayName(text)
if (composerLayout.composerEditText.text.isNullOrBlank()) { if (composerLayout.composerEditText.text.isNullOrBlank()) {
// Ensure displayName will not be interpreted as a Slash command // Ensure displayName will not be interpreted as a Slash command
if (text.startsWith("/")) { if (text.startsWith("/")) {
composerLayout.composerEditText.append("\\") composerLayout.composerEditText.append("\\")
} }
composerLayout.composerEditText.append(sanitizeDisplayName(text) + ": ") SpannableStringBuilder().apply {
append(sanitizeDisplayName)
setSpan(
PillImageSpan(glideRequests, avatarRenderer, requireContext(), member.userId, member),
0,
sanitizeDisplayName.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
append(": ")
}.let {
composerLayout.composerEditText.append(it)
}
} else { } else {
composerLayout.composerEditText.text?.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayName(text) + " ") SpannableStringBuilder().apply {
append(sanitizeDisplayName)
setSpan(
PillImageSpan(glideRequests, avatarRenderer, requireContext(), member.userId, member),
0,
sanitizeDisplayName.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
append(" ")
}.let {
composerLayout.composerEditText.text?.insert(composerLayout.composerEditText.selectionStart, it)
}
// composerLayout.composerEditText.text?.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayName + " ")
} }
// vibrate = true // vibrate = true

View file

@ -34,6 +34,7 @@ import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.session.file.FileService
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.message.getFileUrl import im.vector.matrix.android.api.session.room.model.message.getFileUrl
@ -165,6 +166,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
invisibleEventsObservable.accept(action) invisibleEventsObservable.accept(action)
} }
fun getMember(userId: String) : RoomMember? {
return room.getRoomMember(userId)
}
/** /**
* Convert a send mode to a draft and save the draft * Convert a send mode to a draft and save the draft
*/ */
@ -355,7 +359,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
if (inReplyTo != null) { if (inReplyTo != null) {
// TODO check if same content? // TODO check if same content?
room.getTimeLineEvent(inReplyTo)?.let { room.getTimeLineEvent(inReplyTo)?.let {
room.editReply(state.sendMode.timelineEvent, it, action.text) room.editReply(state.sendMode.timelineEvent, it, action.text.toString())
} }
} else { } else {
val messageContent: MessageContent? = val messageContent: MessageContent? =
@ -380,7 +384,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
?: state.sendMode.timelineEvent.root.getClearContent().toModel() ?: state.sendMode.timelineEvent.root.getClearContent().toModel()
val textMsg = messageContent?.body val textMsg = messageContent?.body
val finalText = legacyRiotQuoteText(textMsg, action.text) val finalText = legacyRiotQuoteText(textMsg, action.text.toString())
//TODO check for pills?
// TODO Refactor this, just temporary for quotes // TODO Refactor this, just temporary for quotes
val parser = Parser.builder().build() val parser = Parser.builder().build()
@ -397,7 +403,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} }
is SendMode.REPLY -> { is SendMode.REPLY -> {
state.sendMode.timelineEvent.let { state.sendMode.timelineEvent.let {
room.replyToMessage(it, action.text, action.autoMarkdown) room.replyToMessage(it, action.text.toString(), action.autoMarkdown)
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent) _sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent)
popDraft() popDraft()
} }

View file

@ -26,6 +26,7 @@ import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.text.toSpannable
import androidx.transition.AutoTransition import androidx.transition.AutoTransition
import androidx.transition.Transition import androidx.transition.Transition
import androidx.transition.TransitionManager import androidx.transition.TransitionManager
@ -43,7 +44,7 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib
interface Callback : ComposerEditText.Callback { interface Callback : ComposerEditText.Callback {
fun onCloseRelatedMessage() fun onCloseRelatedMessage()
fun onSendMessage(text: String) fun onSendMessage(text: CharSequence)
fun onAddAttachment() fun onAddAttachment()
} }
@ -86,8 +87,8 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib
} }
sendButton.setOnClickListener { sendButton.setOnClickListener {
val textMessage = text?.toString() ?: "" val textMessage = text?.toSpannable()
callback?.onSendMessage(textMessage) callback?.onSendMessage(textMessage ?: "")
} }
attachmentButton.setOnClickListener { attachmentButton.setOnClickListener {

View file

@ -28,6 +28,7 @@ import androidx.annotation.UiThread
import com.bumptech.glide.request.target.SimpleTarget import com.bumptech.glide.request.target.SimpleTarget
import com.bumptech.glide.request.transition.Transition import com.bumptech.glide.request.transition.Transition
import com.google.android.material.chip.ChipDrawable import com.google.android.material.chip.ChipDrawable
import im.vector.matrix.android.api.session.room.send.UserMentionSpan
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.glide.GlideRequests import im.vector.riotx.core.glide.GlideRequests
@ -37,14 +38,15 @@ import java.lang.ref.WeakReference
/** /**
* This span is able to replace a text by a [ChipDrawable] * This span is able to replace a text by a [ChipDrawable]
* It's needed to call [bind] method to start requesting avatar, otherwise only the placeholder icon will be displayed if not already cached. * It's needed to call [bind] method to start requesting avatar, otherwise only the placeholder icon will be displayed if not already cached.
* Implements UserMentionSpan so that it could be automatically transformed in matrix links and displayed as pills.
*/ */
class PillImageSpan(private val glideRequests: GlideRequests, class PillImageSpan(private val glideRequests: GlideRequests,
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val context: Context, private val context: Context,
private val userId: String, override val userId: String,
private val user: User?) : ReplacementSpan() { private val user: User?) : ReplacementSpan(), UserMentionSpan {
private val displayName by lazy { override val displayName by lazy {
if (user?.displayName.isNullOrEmpty()) userId else user?.displayName!! if (user?.displayName.isNullOrEmpty()) userId else user?.displayName!!
} }