mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 10:25:35 +03:00
Send mention pills from composer
This commit is contained in:
parent
bf9ce4f690
commit
6bd7257cf2
13 changed files with 141 additions and 42 deletions
|
@ -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<Optional<EventAnnotationsSummary>>
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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, "<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?) =
|
||||
text != htmlText && htmlText != "<p>${text.trim()}</p>\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(
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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<ContentAttachmentData>) : RoomDetailAction()
|
||||
data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailAction()
|
||||
data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailAction()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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!!
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue