diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt index 5af5183dfa..385699b4db 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt @@ -72,7 +72,7 @@ interface RelationService { */ fun editTextMessage(targetEventId: String, msgType: String, - newBodyText: String, + newBodyText: CharSequence, newBodyAutoMarkdown: Boolean, compatibilityBodyText: String = "* $newBodyText"): Cancelable @@ -97,12 +97,14 @@ interface RelationService { /** * Reply to an event in the timeline (must be in same room) * 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 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 */ fun replyToMessage(eventReplied: TimelineEvent, - replyText: String, + replyText: CharSequence, autoMarkdown: Boolean = false): Cancelable? fun getEventSummaryLive(eventId: String): LiveData> diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt index 8c783837a2..e45069bcff 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt @@ -29,12 +29,14 @@ interface SendService { /** * 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 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 * @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. @@ -42,7 +44,7 @@ interface SendService { * @param formattedText The formatted body using MessageType#FORMAT_MATRIX_HTML * @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. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserMentionSpan.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserMentionSpan.kt new file mode 100644 index 0000000000..0899e4f27e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserMentionSpan.kt @@ -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 +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt index 11be821d7e..db3b6100a0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt @@ -115,7 +115,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv override fun editTextMessage(targetEventId: String, msgType: String, - newBodyText: String, + newBodyText: CharSequence, newBodyAutoMarkdown: Boolean, compatibilityBodyText: String): Cancelable { val event = eventFactory @@ -164,7 +164,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv .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) ?.also { saveLocalEcho(it) } ?: return null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt index 7c720e56a7..8fad03b588 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt @@ -68,7 +68,7 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private 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 { saveLocalEcho(it) } @@ -76,8 +76,8 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private return sendEvent(event) } - override fun sendFormattedTextMessage(text: String, formattedText: String): Cancelable { - val event = localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText)).also { + override fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String): Cancelable { + val event = localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType).also { saveLocalEcho(it) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index 3fa0dcdca1..4b099a25be 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.session.room.send import android.media.MediaMetadataRetriever +import android.text.SpannableString import androidx.exifinterface.media.ExifInterface import com.zhuinden.monarchy.Monarchy 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.RelationDefaultContent 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.getLastMessageContent import im.vector.matrix.android.internal.database.helper.addSendingEvent @@ -58,37 +60,67 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use // TODO Inject private val renderer = HtmlRenderer.builder().build() - fun createTextEvent(roomId: String, msgType: String, text: String, autoMarkdown: Boolean): Event { - if (msgType == MessageType.MSGTYPE_TEXT) { - return createFormattedTextEvent(roomId, createTextContent(text, autoMarkdown)) + fun createTextEvent(roomId: String, msgType: String, text: CharSequence, autoMarkdown: Boolean): Event { + if (msgType == MessageType.MSGTYPE_TEXT || msgType == MessageType.MSGTYPE_EMOTE) { + 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) } - private fun createTextContent(text: String, autoMarkdown: Boolean): TextContent { + private fun createTextContent(text: CharSequence, autoMarkdown: Boolean): TextContent { 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) - if (isFormattedTextPertinent(text, htmlText)) { - return TextContent(text, htmlText) + if (isFormattedTextPertinent(source, htmlText)) { + return TextContent(source, htmlText) + } + } else { + //Try to detect pills + transformPills(text, "%2\$s")?.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?) = text != htmlText && htmlText != "

${text.trim()}

\n" - fun createFormattedTextEvent(roomId: String, textContent: TextContent): Event { - return createEvent(roomId, textContent.toMessageTextContent()) + fun createFormattedTextEvent(roomId: String, textContent: TextContent, msgType: String): Event { + return createEvent(roomId, textContent.toMessageTextContent(msgType)) } fun createReplaceTextEvent(roomId: String, targetEventId: String, - newBodyText: String, + newBodyText: CharSequence, newBodyAutoMarkdown: Boolean, msgType: String, compatibilityText: String): Event { @@ -279,7 +311,7 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use 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 // TODO Add error/warning logs when any of this is 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 // - val replyFallback = buildReplyFallback(body, userId, replyText) + val replyFallback = buildReplyFallback(body, userId, replyText.toString()) val eventId = eventReplied.root.eventId ?: return null val content = MessageTextContent( diff --git a/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt b/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt index 3f5808949b..bc451f8e84 100644 --- a/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt @@ -27,7 +27,7 @@ object CommandParser { * @param textMessage the text message * @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 if (!textMessage.startsWith("/")) { return ParsedCommand.ErrorNotACommand @@ -76,7 +76,7 @@ object CommandParser { } } 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) } diff --git a/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt b/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt index 02f5abe540..89438c8a9d 100644 --- a/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt +++ b/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt @@ -33,7 +33,7 @@ sealed class ParsedCommand { // Valid commands: - class SendEmote(val message: String) : ParsedCommand() + class SendEmote(val message: CharSequence) : ParsedCommand() class BanUser(val userId: String, val reason: String) : ParsedCommand() class UnbanUser(val userId: String) : ParsedCommand() class SetUserPowerLevel(val userId: String, val powerLevel: Int) : ParsedCommand() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt index 2e59e70d08..0a6321dd57 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt @@ -25,7 +25,7 @@ import im.vector.riotx.core.platform.VectorViewModelAction sealed class RoomDetailAction : VectorViewModelAction { 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) : RoomDetailAction() data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailAction() data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailAction() 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 6fdbf94590..6186bd1ac1 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 @@ -28,6 +28,7 @@ import android.os.Bundle import android.os.Parcelable import android.text.Editable import android.text.Spannable +import android.text.SpannableStringBuilder import android.view.* import android.view.inputmethod.InputMethodManager import android.widget.TextView @@ -609,7 +610,7 @@ class RoomDetailFragment @Inject constructor( attachmentTypeSelector.show(composerLayout.attachmentButton, keyboardStateUtils.isKeyboardShowing) } - override fun onSendMessage(text: String) { + override fun onSendMessage(text: CharSequence) { if (lockSendButton) { Timber.w("Send button is locked") return @@ -977,7 +978,9 @@ class RoomDetailFragment @Inject constructor( @SuppressLint("SetTextI18n") 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) { @@ -1166,8 +1169,9 @@ class RoomDetailFragment @Inject constructor( * @param text the text to insert. */ // TODO legacy, refactor - private fun insertUserDisplayNameInTextEditor(text: String?) { + private fun insertUserDisplayNameInTextEditor(member: User) { // TODO move logic outside of fragment + val text = member.displayName if (null != text) { // var vibrate = false @@ -1176,19 +1180,44 @@ class RoomDetailFragment @Inject constructor( // current user if (composerLayout.composerEditText.text.isNullOrBlank()) { composerLayout.composerEditText.append(Command.EMOTE.command + " ") - composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length ?: 0) + composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length + ?: 0) // vibrate = true } } else { // another user + val sanitizeDisplayName = sanitizeDisplayName(text) if (composerLayout.composerEditText.text.isNullOrBlank()) { // Ensure displayName will not be interpreted as a Slash command if (text.startsWith("/")) { 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 { - 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 diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index d2c2c7fdde..b8d4ccb7c6 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -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.homeserver.HomeServerCapabilities 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.MessageType 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) } + fun getMember(userId: String) : RoomMember? { + return room.getRoomMember(userId) + } /** * 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) { // TODO check if same content? room.getTimeLineEvent(inReplyTo)?.let { - room.editReply(state.sendMode.timelineEvent, it, action.text) + room.editReply(state.sendMode.timelineEvent, it, action.text.toString()) } } else { val messageContent: MessageContent? = @@ -380,7 +384,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro ?: state.sendMode.timelineEvent.root.getClearContent().toModel() 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 val parser = Parser.builder().build() @@ -397,7 +403,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } is SendMode.REPLY -> { state.sendMode.timelineEvent.let { - room.replyToMessage(it, action.text, action.autoMarkdown) + room.replyToMessage(it, action.text.toString(), action.autoMarkdown) _sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent) popDraft() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt index 32307dc3d4..63e74d6f32 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt @@ -26,6 +26,7 @@ import android.widget.ImageView import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet +import androidx.core.text.toSpannable import androidx.transition.AutoTransition import androidx.transition.Transition import androidx.transition.TransitionManager @@ -43,7 +44,7 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib interface Callback : ComposerEditText.Callback { fun onCloseRelatedMessage() - fun onSendMessage(text: String) + fun onSendMessage(text: CharSequence) fun onAddAttachment() } @@ -86,8 +87,8 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib } sendButton.setOnClickListener { - val textMessage = text?.toString() ?: "" - callback?.onSendMessage(textMessage) + val textMessage = text?.toSpannable() + callback?.onSendMessage(textMessage ?: "") } attachmentButton.setOnClickListener { diff --git a/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt b/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt index bc954204c0..414cd71de7 100644 --- a/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt +++ b/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt @@ -28,6 +28,7 @@ import androidx.annotation.UiThread import com.bumptech.glide.request.target.SimpleTarget import com.bumptech.glide.request.transition.Transition 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.riotx.R 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] * 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, private val avatarRenderer: AvatarRenderer, private val context: Context, - private val userId: String, - private val user: User?) : ReplacementSpan() { + override val userId: String, + private val user: User?) : ReplacementSpan(), UserMentionSpan { - private val displayName by lazy { + override val displayName by lazy { if (user?.displayName.isNullOrEmpty()) userId else user?.displayName!! }