diff --git a/.idea/dictionaries/ganfra.xml b/.idea/dictionaries/ganfra.xml index c3f99f8d70..728c63d678 100644 --- a/.idea/dictionaries/ganfra.xml +++ b/.idea/dictionaries/ganfra.xml @@ -3,6 +3,9 @@ connectable coroutine + linkify + markon + markwon merlins moshi persistor diff --git a/app/src/main/java/im/vector/riotredesign/Riot.kt b/app/src/main/java/im/vector/riotredesign/Riot.kt index acc59cdee1..9a82a20fec 100644 --- a/app/src/main/java/im/vector/riotredesign/Riot.kt +++ b/app/src/main/java/im/vector/riotredesign/Riot.kt @@ -33,14 +33,13 @@ class Riot : Application() { override fun onCreate() { super.onCreate() - applicationContext.setTheme(R.style.Theme_Riot) if (BuildConfig.DEBUG) { Timber.plant(Timber.DebugTree()) Stetho.initializeWithDefaults(this) } AndroidThreeTen.init(this) val appModule = AppModule(applicationContext).definition - val homeModule = HomeModule(applicationContext).definition + val homeModule = HomeModule().definition startKoin(listOf(appModule, homeModule), logger = EmptyLogger()) } diff --git a/app/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt b/app/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt index 3227dc6502..267d1a3ad7 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt @@ -19,6 +19,7 @@ package im.vector.riotredesign.features.home import android.content.Context import android.graphics.drawable.Drawable import android.widget.ImageView +import androidx.annotation.UiThread import androidx.core.content.ContextCompat import com.amulyakhare.textdrawable.TextDrawable import com.bumptech.glide.request.RequestOptions @@ -29,64 +30,49 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.riotredesign.R import im.vector.riotredesign.core.glide.GlideApp import im.vector.riotredesign.core.glide.GlideRequest -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch +import im.vector.riotredesign.core.glide.GlideRequests object AvatarRenderer { + @UiThread fun render(roomMember: RoomMember, imageView: ImageView) { render(roomMember.avatarUrl, roomMember.displayName, imageView) } + @UiThread fun render(roomSummary: RoomSummary, imageView: ImageView) { render(roomSummary.avatarUrl, roomSummary.displayName, imageView) } + @UiThread fun render(avatarUrl: String?, name: String?, imageView: ImageView) { if (name.isNullOrEmpty()) { return } val placeholder = buildPlaceholderDrawable(imageView.context, name) - buildGlideRequest(imageView.context, avatarUrl) + buildGlideRequest(GlideApp.with(imageView), avatarUrl) .placeholder(placeholder) .into(imageView) } - fun load(context: Context, avatarUrl: String?, name: String?, size: Int, callback: Callback) { - if (name.isNullOrEmpty()) { - return - } - val request = buildGlideRequest(context, avatarUrl) - GlobalScope.launch { - val placeholder = buildPlaceholderDrawable(context, name) - callback.onDrawableUpdated(placeholder) - try { - val drawable = request.submit(size, size).get() - callback.onDrawableUpdated(drawable) - } catch (exception: Exception) { - callback.onDrawableUpdated(placeholder) - } - } - } - - private fun buildGlideRequest(context: Context, avatarUrl: String?): GlideRequest { + fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?): GlideRequest { val resolvedUrl = Matrix.getInstance().currentSession.contentUrlResolver().resolveFullSize(avatarUrl) - return GlideApp - .with(context) + return glideRequest .load(resolvedUrl) .apply(RequestOptions.circleCropTransform()) } - private fun buildPlaceholderDrawable(context: Context, name: String): Drawable { + fun buildPlaceholderDrawable(context: Context, text: String): Drawable { val avatarColor = ContextCompat.getColor(context, R.color.pale_teal) - val isNameUserId = MatrixPatterns.isUserId(name) - val firstLetterIndex = if (isNameUserId) 1 else 0 - val firstLetter = name[firstLetterIndex].toString().toUpperCase() - return TextDrawable.builder().buildRound(firstLetter, avatarColor) - } + return if (text.isEmpty()) { + TextDrawable.builder().buildRound("", avatarColor) + } else { + val isUserId = MatrixPatterns.isUserId(text) + val firstLetterIndex = if (isUserId) 1 else 0 + val firstLetter = text[firstLetterIndex].toString().toUpperCase() + TextDrawable.builder().buildRound(firstLetter, avatarColor) + } - interface Callback { - fun onDrawableUpdated(drawable: Drawable?) } } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt b/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt index 71d9f94c21..04a93d666b 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt @@ -16,10 +16,9 @@ package im.vector.riotredesign.features.home -import android.content.Context +import androidx.fragment.app.Fragment +import im.vector.riotredesign.core.glide.GlideApp import im.vector.riotredesign.features.home.group.GroupSummaryController -import im.vector.riotredesign.features.home.group.SelectedGroupStore -import im.vector.riotredesign.features.home.room.VisibleRoomStore import im.vector.riotredesign.features.home.room.detail.timeline.CallItemFactory import im.vector.riotredesign.features.home.room.detail.timeline.DefaultItemFactory import im.vector.riotredesign.features.home.room.detail.timeline.MessageItemFactory @@ -31,12 +30,11 @@ import im.vector.riotredesign.features.home.room.detail.timeline.TimelineDateFor import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController import im.vector.riotredesign.features.home.room.detail.timeline.TimelineItemFactory import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider -import im.vector.riotredesign.features.home.room.list.RoomSummaryComparator import im.vector.riotredesign.features.home.room.list.RoomSummaryController import im.vector.riotredesign.features.html.EventHtmlRenderer import org.koin.dsl.module.module -class HomeModule(context: Context) { +class HomeModule { companion object { const val HOME_SCOPE = "HOME_SCOPE" @@ -49,10 +47,6 @@ class HomeModule(context: Context) { // Activity scope - scope(HOME_SCOPE) { - TimelineDateFormatter(get()) - } - scope(HOME_SCOPE) { HomeNavigator() } @@ -61,50 +55,23 @@ class HomeModule(context: Context) { HomePermalinkHandler(get()) } - scope(HOME_SCOPE) { - RoomNameItemFactory(get()) - } - - scope(HOME_SCOPE) { - RoomTopicItemFactory(get()) - } - - scope(HOME_SCOPE) { - RoomMemberItemFactory(get()) - } - - scope(HOME_SCOPE) { - CallItemFactory(get()) - } - - scope(HOME_SCOPE) { - RoomHistoryVisibilityItemFactory(get()) - } - - scope(HOME_SCOPE) { - DefaultItemFactory() - } - - scope(HOME_SCOPE) { - TimelineMediaSizeProvider() - } - - scope(HOME_SCOPE) { - EventHtmlRenderer(context, get()) - } - - scope(HOME_SCOPE) { - MessageItemFactory(get(), get(), get(), get()) - } - - scope(HOME_SCOPE) { - TimelineItemFactory(get(), get(), get(), get(), get(), get(), get()) - } - // Fragment scopes - scope(ROOM_DETAIL_SCOPE) { - TimelineEventController(get(), get(), get()) + scope(ROOM_DETAIL_SCOPE) { (fragment: Fragment) -> + val eventHtmlRenderer = EventHtmlRenderer(GlideApp.with(fragment), fragment.requireContext(), get()) + val timelineDateFormatter = TimelineDateFormatter(get()) + val timelineMediaSizeProvider = TimelineMediaSizeProvider() + val messageItemFactory = MessageItemFactory(get(), timelineMediaSizeProvider, timelineDateFormatter, eventHtmlRenderer) + + val timelineItemFactory = TimelineItemFactory(messageItemFactory = messageItemFactory, + roomNameItemFactory = RoomNameItemFactory(get()), + roomTopicItemFactory = RoomTopicItemFactory(get()), + roomMemberItemFactory = RoomMemberItemFactory(get()), + roomHistoryVisibilityItemFactory = RoomHistoryVisibilityItemFactory(get()), + callItemFactory = CallItemFactory(get()), + defaultItemFactory = DefaultItemFactory() + ) + TimelineEventController(timelineDateFormatter, timelineItemFactory, timelineMediaSizeProvider) } scope(ROOM_LIST_SCOPE) { diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index 1e1511344c..faf792d706 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -40,6 +40,7 @@ import kotlinx.android.synthetic.main.fragment_room_detail.* import org.koin.android.ext.android.inject import org.koin.android.scope.ext.android.bindScope import org.koin.android.scope.ext.android.getOrCreateScope +import org.koin.core.parameter.parametersOf @Parcelize data class RoomDetailArgs( @@ -60,8 +61,8 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback { } private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel() - private val timelineEventController by inject() - private val homePermalinkHandler by inject() + private val timelineEventController: TimelineEventController by inject { parametersOf(this) } + private val homePermalinkHandler: HomePermalinkHandler by inject() private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageItemFactory.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageItemFactory.kt index d27475f77b..b31ccf940d 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageItemFactory.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageItemFactory.kt @@ -16,13 +16,18 @@ package im.vector.riotredesign.features.home.room.detail.timeline +import android.text.Spannable import android.text.SpannableStringBuilder import android.text.util.Linkify 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.EventType import im.vector.matrix.android.api.session.events.model.toModel -import im.vector.matrix.android.api.session.room.model.message.* +import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageEmoteContent +import im.vector.matrix.android.api.session.room.model.message.MessageImageContent +import im.vector.matrix.android.api.session.room.model.message.MessageNoticeContent +import im.vector.matrix.android.api.session.room.model.message.MessageTextContent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotredesign.R import im.vector.riotredesign.core.epoxy.RiotEpoxyModel @@ -146,7 +151,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider, .informationData(informationData) } - private fun linkifyBody(body: CharSequence, callback: TimelineEventController.Callback?): CharSequence { + private fun linkifyBody(body: CharSequence, callback: TimelineEventController.Callback?): Spannable { val spannable = SpannableStringBuilder(body) MatrixLinkify.addLinks(spannable, object : MatrixPermalinkSpan.Callback { override fun onUrlClicked(url: String) { diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageTextItem.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageTextItem.kt index 8265313dc7..49c754c4dd 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageTextItem.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageTextItem.kt @@ -16,30 +16,59 @@ package im.vector.riotredesign.features.home.room.detail.timeline +import android.text.Spannable import android.widget.ImageView import android.widget.TextView +import androidx.appcompat.widget.AppCompatTextView +import androidx.core.text.PrecomputedTextCompat +import androidx.core.widget.TextViewCompat import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.matrix.android.api.permalinks.MatrixLinkify import im.vector.riotredesign.R +import im.vector.riotredesign.features.html.PillImageSpan +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext @EpoxyModelClass(layout = R.layout.item_timeline_event_text_message) abstract class MessageTextItem : AbsMessageItem() { - @EpoxyAttribute var message: CharSequence? = null + @EpoxyAttribute var message: Spannable? = null @EpoxyAttribute override lateinit var informationData: MessageInformationData override fun bind(holder: Holder) { super.bind(holder) - holder.messageView.text = message MatrixLinkify.addLinkMovementMethod(holder.messageView) + val textFuture = PrecomputedTextCompat.getTextFuture(message ?: "", + TextViewCompat.getTextMetricsParams(holder.messageView), + null) + holder.messageView.setTextFuture(textFuture) + findPillsAndProcess { it.bind(holder.messageView) } + } + + override fun unbind(holder: Holder) { + findPillsAndProcess { it.unbind() } + super.unbind(holder) + } + + private fun findPillsAndProcess(processBlock: (span: PillImageSpan) -> Unit) { + GlobalScope.launch(Dispatchers.Main) { + val pillImageSpans: Array? = withContext(Dispatchers.IO) { + message?.let { spannable -> + spannable.getSpans(0, spannable.length, PillImageSpan::class.java) + } + } + pillImageSpans?.forEach { processBlock(it) } + } } class Holder : AbsMessageItem.Holder() { override val avatarImageView by bind(R.id.messageAvatarImageView) override val memberNameView by bind(R.id.messageMemberNameView) override val timeView by bind(R.id.messageTimeView) - val messageView by bind(R.id.messageTextView) + val messageView by bind(R.id.messageTextView) } diff --git a/app/src/main/java/im/vector/riotredesign/features/html/EventHtmlRenderer.kt b/app/src/main/java/im/vector/riotredesign/features/html/EventHtmlRenderer.kt index dcbe28f11b..27e00b54c6 100644 --- a/app/src/main/java/im/vector/riotredesign/features/html/EventHtmlRenderer.kt +++ b/app/src/main/java/im/vector/riotredesign/features/html/EventHtmlRenderer.kt @@ -19,10 +19,10 @@ package im.vector.riotredesign.features.html import android.content.Context -import android.text.style.ImageSpan import im.vector.matrix.android.api.permalinks.PermalinkData import im.vector.matrix.android.api.permalinks.PermalinkParser import im.vector.matrix.android.api.session.Session +import im.vector.riotredesign.core.glide.GlideRequests import org.commonmark.node.BlockQuote import org.commonmark.node.HtmlBlock import org.commonmark.node.HtmlInline @@ -49,11 +49,12 @@ import ru.noties.markwon.html.tag.SuperScriptHandler import ru.noties.markwon.html.tag.UnderlineHandler import java.util.Arrays.asList -class EventHtmlRenderer(private val context: Context, - private val session: Session) { +class EventHtmlRenderer(glideRequests: GlideRequests, + context: Context, + session: Session) { private val markwon = Markwon.builder(context) - .usePlugin(MatrixPlugin.create(context, session)) + .usePlugin(MatrixPlugin.create(glideRequests, context, session)) .build() fun render(text: String): CharSequence { @@ -62,7 +63,8 @@ class EventHtmlRenderer(private val context: Context, } -private class MatrixPlugin private constructor(private val context: Context, +private class MatrixPlugin private constructor(private val glideRequests: GlideRequests, + private val context: Context, private val session: Session) : AbstractMarkwonPlugin() { override fun configureConfiguration(builder: MarkwonConfiguration.Builder) { @@ -76,7 +78,7 @@ private class MatrixPlugin private constructor(private val context: Context, ImageHandler.create()) .addHandler( "a", - MxLinkHandler(context, session)) + MxLinkHandler(glideRequests, context, session)) .addHandler( "blockquote", BlockquoteHandler()) @@ -128,13 +130,15 @@ private class MatrixPlugin private constructor(private val context: Context, companion object { - fun create(context: Context, session: Session): MatrixPlugin { - return MatrixPlugin(context, session) + fun create(glideRequests: GlideRequests, context: Context, session: Session): MatrixPlugin { + return MatrixPlugin(glideRequests, context, session) } } } -private class MxLinkHandler(private val context: Context, private val session: Session) : TagHandler() { +private class MxLinkHandler(private val glideRequests: GlideRequests, + private val context: Context, + private val session: Session) : TagHandler() { private val linkHandler = LinkHandler() @@ -145,8 +149,7 @@ private class MxLinkHandler(private val context: Context, private val session: S when (permalinkData) { is PermalinkData.UserLink -> { val user = session.getUser(permalinkData.userId) ?: return - val drawable = PillDrawableFactory.create(context, permalinkData.userId, user) - val span = ImageSpan(drawable) + val span = PillImageSpan(glideRequests, context, permalinkData.userId, user) SpannableBuilder.setSpans( visitor.builder(), span, diff --git a/app/src/main/java/im/vector/riotredesign/features/html/PillDrawableFactory.kt b/app/src/main/java/im/vector/riotredesign/features/html/PillDrawableFactory.kt deleted file mode 100644 index 9b87c67135..0000000000 --- a/app/src/main/java/im/vector/riotredesign/features/html/PillDrawableFactory.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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.riotredesign.features.html - -import android.content.Context -import android.graphics.drawable.Drawable -import com.google.android.material.chip.ChipDrawable -import im.vector.matrix.android.api.session.user.model.User -import im.vector.riotredesign.R -import im.vector.riotredesign.features.home.AvatarRenderer -import java.lang.ref.WeakReference - -object PillDrawableFactory { - - fun create(context: Context, userId: String, user: User?): Drawable { - val textPadding = context.resources.getDimension(R.dimen.pill_text_padding) - - val chipDrawable = ChipDrawable.createFromResource(context, R.xml.pill_view).apply { - setText(user?.displayName ?: userId) - textEndPadding = textPadding - textStartPadding = textPadding - setChipMinHeightResource(R.dimen.pill_min_height) - setChipIconSizeResource(R.dimen.pill_avatar_size) - setBounds(0, 0, intrinsicWidth, intrinsicHeight) - } - val avatarRendererCallback = AvatarRendererChipCallback(chipDrawable) - AvatarRenderer.load(context, user?.avatarUrl, user?.displayName, 80, avatarRendererCallback) - return chipDrawable - } - - private class AvatarRendererChipCallback(chipDrawable: ChipDrawable) : AvatarRenderer.Callback { - - private val weakChipDrawable = WeakReference(chipDrawable) - - override fun onDrawableUpdated(drawable: Drawable?) { - weakChipDrawable.get()?.apply { - chipIcon = drawable - setBounds(0, 0, intrinsicWidth, intrinsicHeight) - } - } - - } - -} - diff --git a/app/src/main/java/im/vector/riotredesign/features/html/PillImageSpan.kt b/app/src/main/java/im/vector/riotredesign/features/html/PillImageSpan.kt new file mode 100644 index 0000000000..cbc0251a08 --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/html/PillImageSpan.kt @@ -0,0 +1,129 @@ +/* + * 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.riotredesign.features.html + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.drawable.Drawable +import android.text.style.ReplacementSpan +import android.widget.TextView +import androidx.annotation.MainThread +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.user.model.User +import im.vector.riotredesign.R +import im.vector.riotredesign.core.glide.GlideRequests +import im.vector.riotredesign.features.home.AvatarRenderer +import java.lang.ref.WeakReference + +class PillImageSpan(private val glideRequests: GlideRequests, + private val context: Context, + private val userId: String, + private val user: User?) : ReplacementSpan() { + + private val pillDrawable = createChipDrawable(context, userId, user) + private val target = PillImageSpanTarget(this) + private var tv: WeakReference? = null + + @MainThread + fun bind(textView: TextView) { + tv = WeakReference(textView) + AvatarRenderer.buildGlideRequest(glideRequests, user?.avatarUrl).into(target) + } + + @MainThread + fun unbind() { + glideRequests.clear(target) + tv = null + } + + @MainThread + private fun updateAvatarDrawable(drawable: Drawable?) { + pillDrawable.apply { + chipIcon = drawable + } + tv?.get()?.apply { + invalidate() + } + } + + override fun getSize(paint: Paint, text: CharSequence, + start: Int, + end: Int, + fm: Paint.FontMetricsInt?): Int { + val rect = pillDrawable.bounds + if (fm != null) { + fm.ascent = -rect.bottom + fm.descent = 0 + fm.top = fm.ascent + fm.bottom = 0 + } + return rect.right + } + + override fun draw(canvas: Canvas, text: CharSequence, + start: Int, + end: Int, + x: Float, + top: Int, + y: Int, + bottom: Int, + paint: Paint) { + + canvas.save() + val transY = bottom - pillDrawable.bounds.bottom + canvas.translate(x, transY.toFloat()) + pillDrawable.draw(canvas) + canvas.restore() + } + + private fun createChipDrawable(context: Context, userId: String, user: User?): ChipDrawable { + val textPadding = context.resources.getDimension(R.dimen.pill_text_padding) + val displayName = if (user?.displayName.isNullOrEmpty()) userId else user?.displayName!! + return ChipDrawable.createFromResource(context, R.xml.pill_view).apply { + setText(displayName) + textEndPadding = textPadding + textStartPadding = textPadding + setChipMinHeightResource(R.dimen.pill_min_height) + setChipIconSizeResource(R.dimen.pill_avatar_size) + chipIcon = AvatarRenderer.buildPlaceholderDrawable(context, displayName) + setBounds(0, 0, intrinsicWidth, intrinsicHeight) + } + } + + private class PillImageSpanTarget(pillImageSpan: PillImageSpan) : SimpleTarget() { + + private val pillImageSpan = WeakReference(pillImageSpan) + + override fun onResourceReady(drawable: Drawable, transition: Transition?) { + updateWith(drawable) + } + + override fun onLoadCleared(placeholder: Drawable?) { + updateWith(placeholder) + } + + private fun updateWith(drawable: Drawable?) { + pillImageSpan.get()?.apply { + this.updateAvatarDrawable(drawable) + } + } + } + +} \ No newline at end of file