forked from GitHub-Mirror/riotX-android
Pills : try to optimize memory and get better perf. Still need to rework a bit.
This commit is contained in:
parent
3d7562ea8e
commit
fffdf4b8c1
3
.idea/dictionaries/ganfra.xml
generated
3
.idea/dictionaries/ganfra.xml
generated
@ -3,6 +3,9 @@
|
|||||||
<words>
|
<words>
|
||||||
<w>connectable</w>
|
<w>connectable</w>
|
||||||
<w>coroutine</w>
|
<w>coroutine</w>
|
||||||
|
<w>linkify</w>
|
||||||
|
<w>markon</w>
|
||||||
|
<w>markwon</w>
|
||||||
<w>merlins</w>
|
<w>merlins</w>
|
||||||
<w>moshi</w>
|
<w>moshi</w>
|
||||||
<w>persistor</w>
|
<w>persistor</w>
|
||||||
|
@ -33,14 +33,13 @@ class Riot : Application() {
|
|||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
applicationContext.setTheme(R.style.Theme_Riot)
|
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
Timber.plant(Timber.DebugTree())
|
Timber.plant(Timber.DebugTree())
|
||||||
Stetho.initializeWithDefaults(this)
|
Stetho.initializeWithDefaults(this)
|
||||||
}
|
}
|
||||||
AndroidThreeTen.init(this)
|
AndroidThreeTen.init(this)
|
||||||
val appModule = AppModule(applicationContext).definition
|
val appModule = AppModule(applicationContext).definition
|
||||||
val homeModule = HomeModule(applicationContext).definition
|
val homeModule = HomeModule().definition
|
||||||
startKoin(listOf(appModule, homeModule), logger = EmptyLogger())
|
startKoin(listOf(appModule, homeModule), logger = EmptyLogger())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ package im.vector.riotredesign.features.home
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
import androidx.annotation.UiThread
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.amulyakhare.textdrawable.TextDrawable
|
import com.amulyakhare.textdrawable.TextDrawable
|
||||||
import com.bumptech.glide.request.RequestOptions
|
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.R
|
||||||
import im.vector.riotredesign.core.glide.GlideApp
|
import im.vector.riotredesign.core.glide.GlideApp
|
||||||
import im.vector.riotredesign.core.glide.GlideRequest
|
import im.vector.riotredesign.core.glide.GlideRequest
|
||||||
import kotlinx.coroutines.GlobalScope
|
import im.vector.riotredesign.core.glide.GlideRequests
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
object AvatarRenderer {
|
object AvatarRenderer {
|
||||||
|
|
||||||
|
@UiThread
|
||||||
fun render(roomMember: RoomMember, imageView: ImageView) {
|
fun render(roomMember: RoomMember, imageView: ImageView) {
|
||||||
render(roomMember.avatarUrl, roomMember.displayName, imageView)
|
render(roomMember.avatarUrl, roomMember.displayName, imageView)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UiThread
|
||||||
fun render(roomSummary: RoomSummary, imageView: ImageView) {
|
fun render(roomSummary: RoomSummary, imageView: ImageView) {
|
||||||
render(roomSummary.avatarUrl, roomSummary.displayName, imageView)
|
render(roomSummary.avatarUrl, roomSummary.displayName, imageView)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UiThread
|
||||||
fun render(avatarUrl: String?, name: String?, imageView: ImageView) {
|
fun render(avatarUrl: String?, name: String?, imageView: ImageView) {
|
||||||
if (name.isNullOrEmpty()) {
|
if (name.isNullOrEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val placeholder = buildPlaceholderDrawable(imageView.context, name)
|
val placeholder = buildPlaceholderDrawable(imageView.context, name)
|
||||||
buildGlideRequest(imageView.context, avatarUrl)
|
buildGlideRequest(GlideApp.with(imageView), avatarUrl)
|
||||||
.placeholder(placeholder)
|
.placeholder(placeholder)
|
||||||
.into(imageView)
|
.into(imageView)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun load(context: Context, avatarUrl: String?, name: String?, size: Int, callback: Callback) {
|
fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?): GlideRequest<Drawable> {
|
||||||
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<Drawable> {
|
|
||||||
val resolvedUrl = Matrix.getInstance().currentSession.contentUrlResolver().resolveFullSize(avatarUrl)
|
val resolvedUrl = Matrix.getInstance().currentSession.contentUrlResolver().resolveFullSize(avatarUrl)
|
||||||
return GlideApp
|
return glideRequest
|
||||||
.with(context)
|
|
||||||
.load(resolvedUrl)
|
.load(resolvedUrl)
|
||||||
.apply(RequestOptions.circleCropTransform())
|
.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 avatarColor = ContextCompat.getColor(context, R.color.pale_teal)
|
||||||
val isNameUserId = MatrixPatterns.isUserId(name)
|
return if (text.isEmpty()) {
|
||||||
val firstLetterIndex = if (isNameUserId) 1 else 0
|
TextDrawable.builder().buildRound("", avatarColor)
|
||||||
val firstLetter = name[firstLetterIndex].toString().toUpperCase()
|
} else {
|
||||||
return TextDrawable.builder().buildRound(firstLetter, avatarColor)
|
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?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -16,10 +16,9 @@
|
|||||||
|
|
||||||
package im.vector.riotredesign.features.home
|
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.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.CallItemFactory
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.DefaultItemFactory
|
import im.vector.riotredesign.features.home.room.detail.timeline.DefaultItemFactory
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.MessageItemFactory
|
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.TimelineEventController
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineItemFactory
|
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.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.home.room.list.RoomSummaryController
|
||||||
import im.vector.riotredesign.features.html.EventHtmlRenderer
|
import im.vector.riotredesign.features.html.EventHtmlRenderer
|
||||||
import org.koin.dsl.module.module
|
import org.koin.dsl.module.module
|
||||||
|
|
||||||
class HomeModule(context: Context) {
|
class HomeModule {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val HOME_SCOPE = "HOME_SCOPE"
|
const val HOME_SCOPE = "HOME_SCOPE"
|
||||||
@ -49,10 +47,6 @@ class HomeModule(context: Context) {
|
|||||||
|
|
||||||
// Activity scope
|
// Activity scope
|
||||||
|
|
||||||
scope(HOME_SCOPE) {
|
|
||||||
TimelineDateFormatter(get())
|
|
||||||
}
|
|
||||||
|
|
||||||
scope(HOME_SCOPE) {
|
scope(HOME_SCOPE) {
|
||||||
HomeNavigator()
|
HomeNavigator()
|
||||||
}
|
}
|
||||||
@ -61,50 +55,23 @@ class HomeModule(context: Context) {
|
|||||||
HomePermalinkHandler(get())
|
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
|
// Fragment scopes
|
||||||
|
|
||||||
scope(ROOM_DETAIL_SCOPE) {
|
scope(ROOM_DETAIL_SCOPE) { (fragment: Fragment) ->
|
||||||
TimelineEventController(get(), get(), get())
|
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) {
|
scope(ROOM_LIST_SCOPE) {
|
||||||
|
@ -40,6 +40,7 @@ import kotlinx.android.synthetic.main.fragment_room_detail.*
|
|||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.koin.android.scope.ext.android.bindScope
|
import org.koin.android.scope.ext.android.bindScope
|
||||||
import org.koin.android.scope.ext.android.getOrCreateScope
|
import org.koin.android.scope.ext.android.getOrCreateScope
|
||||||
|
import org.koin.core.parameter.parametersOf
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class RoomDetailArgs(
|
data class RoomDetailArgs(
|
||||||
@ -60,8 +61,8 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel()
|
private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel()
|
||||||
private val timelineEventController by inject<TimelineEventController>()
|
private val timelineEventController: TimelineEventController by inject { parametersOf(this) }
|
||||||
private val homePermalinkHandler by inject<HomePermalinkHandler>()
|
private val homePermalinkHandler: HomePermalinkHandler by inject()
|
||||||
|
|
||||||
private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
|
private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
|
||||||
|
|
||||||
|
@ -16,13 +16,18 @@
|
|||||||
|
|
||||||
package im.vector.riotredesign.features.home.room.detail.timeline
|
package im.vector.riotredesign.features.home.room.detail.timeline
|
||||||
|
|
||||||
|
import android.text.Spannable
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.util.Linkify
|
import android.text.util.Linkify
|
||||||
import im.vector.matrix.android.api.permalinks.MatrixLinkify
|
import im.vector.matrix.android.api.permalinks.MatrixLinkify
|
||||||
import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan
|
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.EventType
|
||||||
import im.vector.matrix.android.api.session.events.model.toModel
|
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.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.riotredesign.R
|
import im.vector.riotredesign.R
|
||||||
import im.vector.riotredesign.core.epoxy.RiotEpoxyModel
|
import im.vector.riotredesign.core.epoxy.RiotEpoxyModel
|
||||||
@ -146,7 +151,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||||||
.informationData(informationData)
|
.informationData(informationData)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun linkifyBody(body: CharSequence, callback: TimelineEventController.Callback?): CharSequence {
|
private fun linkifyBody(body: CharSequence, callback: TimelineEventController.Callback?): Spannable {
|
||||||
val spannable = SpannableStringBuilder(body)
|
val spannable = SpannableStringBuilder(body)
|
||||||
MatrixLinkify.addLinks(spannable, object : MatrixPermalinkSpan.Callback {
|
MatrixLinkify.addLinks(spannable, object : MatrixPermalinkSpan.Callback {
|
||||||
override fun onUrlClicked(url: String) {
|
override fun onUrlClicked(url: String) {
|
||||||
|
@ -16,30 +16,59 @@
|
|||||||
|
|
||||||
package im.vector.riotredesign.features.home.room.detail.timeline
|
package im.vector.riotredesign.features.home.room.detail.timeline
|
||||||
|
|
||||||
|
import android.text.Spannable
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
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.EpoxyAttribute
|
||||||
import com.airbnb.epoxy.EpoxyModelClass
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
import im.vector.matrix.android.api.permalinks.MatrixLinkify
|
import im.vector.matrix.android.api.permalinks.MatrixLinkify
|
||||||
import im.vector.riotredesign.R
|
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)
|
@EpoxyModelClass(layout = R.layout.item_timeline_event_text_message)
|
||||||
abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
||||||
|
|
||||||
@EpoxyAttribute var message: CharSequence? = null
|
@EpoxyAttribute var message: Spannable? = null
|
||||||
@EpoxyAttribute override lateinit var informationData: MessageInformationData
|
@EpoxyAttribute override lateinit var informationData: MessageInformationData
|
||||||
|
|
||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
super.bind(holder)
|
super.bind(holder)
|
||||||
holder.messageView.text = message
|
|
||||||
MatrixLinkify.addLinkMovementMethod(holder.messageView)
|
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<PillImageSpan>? = withContext(Dispatchers.IO) {
|
||||||
|
message?.let { spannable ->
|
||||||
|
spannable.getSpans(0, spannable.length, PillImageSpan::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pillImageSpans?.forEach { processBlock(it) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Holder : AbsMessageItem.Holder() {
|
class Holder : AbsMessageItem.Holder() {
|
||||||
override val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
|
override val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
|
||||||
override val memberNameView by bind<TextView>(R.id.messageMemberNameView)
|
override val memberNameView by bind<TextView>(R.id.messageMemberNameView)
|
||||||
override val timeView by bind<TextView>(R.id.messageTimeView)
|
override val timeView by bind<TextView>(R.id.messageTimeView)
|
||||||
val messageView by bind<TextView>(R.id.messageTextView)
|
val messageView by bind<AppCompatTextView>(R.id.messageTextView)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,10 +19,10 @@
|
|||||||
package im.vector.riotredesign.features.html
|
package im.vector.riotredesign.features.html
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.text.style.ImageSpan
|
|
||||||
import im.vector.matrix.android.api.permalinks.PermalinkData
|
import im.vector.matrix.android.api.permalinks.PermalinkData
|
||||||
import im.vector.matrix.android.api.permalinks.PermalinkParser
|
import im.vector.matrix.android.api.permalinks.PermalinkParser
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
|
import im.vector.riotredesign.core.glide.GlideRequests
|
||||||
import org.commonmark.node.BlockQuote
|
import org.commonmark.node.BlockQuote
|
||||||
import org.commonmark.node.HtmlBlock
|
import org.commonmark.node.HtmlBlock
|
||||||
import org.commonmark.node.HtmlInline
|
import org.commonmark.node.HtmlInline
|
||||||
@ -49,11 +49,12 @@ import ru.noties.markwon.html.tag.SuperScriptHandler
|
|||||||
import ru.noties.markwon.html.tag.UnderlineHandler
|
import ru.noties.markwon.html.tag.UnderlineHandler
|
||||||
import java.util.Arrays.asList
|
import java.util.Arrays.asList
|
||||||
|
|
||||||
class EventHtmlRenderer(private val context: Context,
|
class EventHtmlRenderer(glideRequests: GlideRequests,
|
||||||
private val session: Session) {
|
context: Context,
|
||||||
|
session: Session) {
|
||||||
|
|
||||||
private val markwon = Markwon.builder(context)
|
private val markwon = Markwon.builder(context)
|
||||||
.usePlugin(MatrixPlugin.create(context, session))
|
.usePlugin(MatrixPlugin.create(glideRequests, context, session))
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun render(text: String): CharSequence {
|
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() {
|
private val session: Session) : AbstractMarkwonPlugin() {
|
||||||
|
|
||||||
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
|
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
|
||||||
@ -76,7 +78,7 @@ private class MatrixPlugin private constructor(private val context: Context,
|
|||||||
ImageHandler.create())
|
ImageHandler.create())
|
||||||
.addHandler(
|
.addHandler(
|
||||||
"a",
|
"a",
|
||||||
MxLinkHandler(context, session))
|
MxLinkHandler(glideRequests, context, session))
|
||||||
.addHandler(
|
.addHandler(
|
||||||
"blockquote",
|
"blockquote",
|
||||||
BlockquoteHandler())
|
BlockquoteHandler())
|
||||||
@ -128,13 +130,15 @@ private class MatrixPlugin private constructor(private val context: Context,
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun create(context: Context, session: Session): MatrixPlugin {
|
fun create(glideRequests: GlideRequests, context: Context, session: Session): MatrixPlugin {
|
||||||
return MatrixPlugin(context, session)
|
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()
|
private val linkHandler = LinkHandler()
|
||||||
|
|
||||||
@ -145,8 +149,7 @@ private class MxLinkHandler(private val context: Context, private val session: S
|
|||||||
when (permalinkData) {
|
when (permalinkData) {
|
||||||
is PermalinkData.UserLink -> {
|
is PermalinkData.UserLink -> {
|
||||||
val user = session.getUser(permalinkData.userId) ?: return
|
val user = session.getUser(permalinkData.userId) ?: return
|
||||||
val drawable = PillDrawableFactory.create(context, permalinkData.userId, user)
|
val span = PillImageSpan(glideRequests, context, permalinkData.userId, user)
|
||||||
val span = ImageSpan(drawable)
|
|
||||||
SpannableBuilder.setSpans(
|
SpannableBuilder.setSpans(
|
||||||
visitor.builder(),
|
visitor.builder(),
|
||||||
span,
|
span,
|
||||||
|
@ -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>(chipDrawable)
|
|
||||||
|
|
||||||
override fun onDrawableUpdated(drawable: Drawable?) {
|
|
||||||
weakChipDrawable.get()?.apply {
|
|
||||||
chipIcon = drawable
|
|
||||||
setBounds(0, 0, intrinsicWidth, intrinsicHeight)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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<TextView>? = 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<Drawable>() {
|
||||||
|
|
||||||
|
private val pillImageSpan = WeakReference(pillImageSpan)
|
||||||
|
|
||||||
|
override fun onResourceReady(drawable: Drawable, transition: Transition<in Drawable>?) {
|
||||||
|
updateWith(drawable)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLoadCleared(placeholder: Drawable?) {
|
||||||
|
updateWith(placeholder)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateWith(drawable: Drawable?) {
|
||||||
|
pillImageSpan.get()?.apply {
|
||||||
|
this.updateAvatarDrawable(drawable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user