Timeline: first version of diff/cache
This commit is contained in:
@@ -18,6 +18,7 @@ dependencies {
|
||||
implementation(libs.timber)
|
||||
implementation(libs.datetime)
|
||||
implementation(libs.accompanist.flowlayout)
|
||||
implementation(libs.androidx.recyclerview)
|
||||
implementation("org.jsoup:jsoup:1.15.3")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.3")
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package io.element.android.x.features.messages
|
||||
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import io.element.android.x.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.x.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.x.features.messages.diff.CacheInvalidator
|
||||
import io.element.android.x.features.messages.diff.MatrixTimelineItemsDiffCallback
|
||||
import io.element.android.x.features.messages.model.AggregatedReaction
|
||||
import io.element.android.x.features.messages.model.MessagesItemGroupPosition
|
||||
import io.element.android.x.features.messages.model.MessagesItemReactionState
|
||||
@@ -12,44 +15,92 @@ import io.element.android.x.matrix.media.MediaResolver
|
||||
import io.element.android.x.matrix.room.MatrixRoom
|
||||
import io.element.android.x.matrix.timeline.MatrixTimelineItem
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.matrix.rustcomponents.sdk.FormattedBody
|
||||
import org.matrix.rustcomponents.sdk.MessageFormat
|
||||
import org.matrix.rustcomponents.sdk.MessageType
|
||||
import org.matrix.rustcomponents.sdk.TimelineKey
|
||||
import timber.log.Timber
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
class MessageTimelineItemStateFactory(
|
||||
private val client: MatrixClient,
|
||||
private val room: MatrixRoom,
|
||||
private val dispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
|
||||
private val timelineItemCaches = arrayListOf<MessagesTimelineItemState?>()
|
||||
private var currentSnapshot: List<MatrixTimelineItem> = emptyList()
|
||||
|
||||
private val lock = Mutex()
|
||||
private val cacheInvalidator = CacheInvalidator(timelineItemCaches)
|
||||
|
||||
suspend fun create(
|
||||
timelineItems: List<MatrixTimelineItem>,
|
||||
): List<MessagesTimelineItemState> =
|
||||
withContext(dispatcher) {
|
||||
val messagesTimelineItemState = ArrayList<MessagesTimelineItemState>()
|
||||
for (index in timelineItems.indices.reversed()) {
|
||||
val currentTimelineItem = timelineItems[index]
|
||||
val timelineItemState = when (currentTimelineItem) {
|
||||
is MatrixTimelineItem.Event -> {
|
||||
buildMessageEvent(
|
||||
currentTimelineItem,
|
||||
index,
|
||||
timelineItems,
|
||||
)
|
||||
}
|
||||
is MatrixTimelineItem.Virtual -> MessagesTimelineItemState.Virtual(
|
||||
"virtual_item_$index"
|
||||
)
|
||||
MatrixTimelineItem.Other -> continue
|
||||
}
|
||||
messagesTimelineItemState.add(timelineItemState)
|
||||
lock.withLock {
|
||||
calculateAndApplyDiff(timelineItems)
|
||||
getOrCreateFromCache(timelineItems)
|
||||
}
|
||||
messagesTimelineItemState
|
||||
}
|
||||
|
||||
private suspend fun getOrCreateFromCache(timelineItems: List<MatrixTimelineItem>): List<MessagesTimelineItemState> {
|
||||
val messagesTimelineItemState = ArrayList<MessagesTimelineItemState>()
|
||||
for (index in timelineItemCaches.indices.reversed()) {
|
||||
val cacheItem = timelineItemCaches[index]
|
||||
if (cacheItem == null) {
|
||||
buildAndCacheItem(timelineItems, index)?.also { timelineItemState ->
|
||||
messagesTimelineItemState.add(timelineItemState)
|
||||
}
|
||||
} else {
|
||||
messagesTimelineItemState.add(cacheItem)
|
||||
}
|
||||
}
|
||||
return messagesTimelineItemState
|
||||
}
|
||||
|
||||
|
||||
private fun calculateAndApplyDiff(timelineItems: List<MatrixTimelineItem>) {
|
||||
val timeToDiff = measureTimeMillis {
|
||||
val diffCallback =
|
||||
MatrixTimelineItemsDiffCallback(
|
||||
oldList = currentSnapshot,
|
||||
newList = timelineItems
|
||||
)
|
||||
|
||||
val diffResult = DiffUtil.calculateDiff(diffCallback, false)
|
||||
currentSnapshot = timelineItems
|
||||
diffResult.dispatchUpdatesTo(cacheInvalidator)
|
||||
}
|
||||
Timber.v("Time to apply diff on new list of ${timelineItems.size} items: $timeToDiff ms")
|
||||
}
|
||||
|
||||
private suspend fun buildAndCacheItem(
|
||||
timelineItems: List<MatrixTimelineItem>,
|
||||
index: Int
|
||||
): MessagesTimelineItemState? {
|
||||
val timelineItemState =
|
||||
when (val currentTimelineItem = timelineItems[index]) {
|
||||
is MatrixTimelineItem.Event -> {
|
||||
buildMessageEvent(
|
||||
currentTimelineItem,
|
||||
index,
|
||||
timelineItems,
|
||||
)
|
||||
}
|
||||
is MatrixTimelineItem.Virtual -> MessagesTimelineItemState.Virtual(
|
||||
"virtual_item_$index"
|
||||
)
|
||||
MatrixTimelineItem.Other -> null
|
||||
}
|
||||
timelineItemCaches[index] = timelineItemState
|
||||
return timelineItemState
|
||||
}
|
||||
|
||||
private suspend fun buildMessageEvent(
|
||||
currentTimelineItem: MatrixTimelineItem.Event,
|
||||
index: Int,
|
||||
@@ -62,12 +113,8 @@ class MessageTimelineItemStateFactory(
|
||||
val senderAvatarUrl = room.userAvatarUrl(currentSender).getOrNull()
|
||||
val senderAvatarData =
|
||||
loadAvatarData(senderDisplayName ?: currentSender, senderAvatarUrl)
|
||||
val uniqueId = when (val eventKey = currentTimelineItem.event.key()) {
|
||||
is TimelineKey.TransactionId -> eventKey.txnId
|
||||
is TimelineKey.EventId -> eventKey.eventId
|
||||
}
|
||||
return MessagesTimelineItemState.MessageEvent(
|
||||
id = uniqueId,
|
||||
id = currentTimelineItem.uniqueId,
|
||||
senderId = currentSender,
|
||||
senderDisplayName = senderDisplayName,
|
||||
senderAvatar = senderAvatarData,
|
||||
@@ -166,6 +213,4 @@ class MessageTimelineItemStateFactory(
|
||||
.resolve(url, kind = MediaResolver.Kind.Thumbnail(size.value))
|
||||
return AvatarData(name, model, size)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package io.element.android.x.features.messages.diff
|
||||
|
||||
import androidx.recyclerview.widget.ListUpdateCallback
|
||||
import io.element.android.x.features.messages.model.MessagesTimelineItemState
|
||||
import timber.log.Timber
|
||||
|
||||
internal class CacheInvalidator(private val timelineItemCache: MutableList<MessagesTimelineItemState?>) :
|
||||
ListUpdateCallback {
|
||||
|
||||
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
||||
Timber.v("onChanged(position= $position, count= $count")
|
||||
(position until position + count).forEach {
|
||||
// Invalidate cache
|
||||
timelineItemCache[it] = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
||||
Timber.v("onMoved(fromPosition= $fromPosition, toPosition= $toPosition")
|
||||
val model = timelineItemCache.removeAt(fromPosition)
|
||||
timelineItemCache.add(toPosition, model)
|
||||
}
|
||||
|
||||
override fun onInserted(position: Int, count: Int) {
|
||||
Timber.v("onInserted(position= $position, count= $count")
|
||||
repeat(count) {
|
||||
timelineItemCache.add(position, null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRemoved(position: Int, count: Int) {
|
||||
Timber.v("onRemoved(position= $position, count= $count")
|
||||
repeat(count) {
|
||||
timelineItemCache.removeAt(position)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package io.element.android.x.features.messages.diff
|
||||
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import io.element.android.x.matrix.timeline.MatrixTimelineItem
|
||||
|
||||
internal class MatrixTimelineItemsDiffCallback(
|
||||
private val oldList: List<MatrixTimelineItem>,
|
||||
private val newList: List<MatrixTimelineItem>
|
||||
) : DiffUtil.Callback() {
|
||||
|
||||
override fun getOldListSize(): Int {
|
||||
return oldList.size
|
||||
}
|
||||
|
||||
override fun getNewListSize(): Int {
|
||||
return newList.size
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
val oldItem = oldList.getOrNull(oldItemPosition)
|
||||
val newItem = newList.getOrNull(newItemPosition)
|
||||
return if (oldItem is MatrixTimelineItem.Event && newItem is MatrixTimelineItem.Event) {
|
||||
oldItem.uniqueId == newItem.uniqueId
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
val oldItem = oldList.getOrNull(oldItemPosition)
|
||||
val newItem = newList.getOrNull(newItemPosition)
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
}
|
||||
@@ -10,6 +10,7 @@ material = "1.6.1"
|
||||
corektx = "1.9.0"
|
||||
datastore = "1.0.0"
|
||||
constraintlayout = "2.1.4"
|
||||
recyclerview = "1.2.1"
|
||||
|
||||
# Compose
|
||||
compose_compiler = "1.3.2"
|
||||
@@ -52,6 +53,7 @@ androidx_corektx = { module = "androidx.core:core-ktx", version.ref = "corektx"
|
||||
androidx_datastore_preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" }
|
||||
androidx_datastore_datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" }
|
||||
androidx_constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" }
|
||||
androidx_recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }
|
||||
|
||||
androidx_compose_bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose_bom" }
|
||||
androidx_compose_foundation = { group = "androidx.compose.foundation", name = "foundation" }
|
||||
|
||||
@@ -2,9 +2,17 @@ package io.element.android.x.matrix.timeline
|
||||
|
||||
import org.matrix.rustcomponents.sdk.EventTimelineItem
|
||||
import org.matrix.rustcomponents.sdk.TimelineItem
|
||||
import org.matrix.rustcomponents.sdk.TimelineKey
|
||||
|
||||
sealed interface MatrixTimelineItem {
|
||||
data class Event(val event: EventTimelineItem) : MatrixTimelineItem
|
||||
data class Event(val event: EventTimelineItem) : MatrixTimelineItem {
|
||||
val uniqueId: String
|
||||
get() = when (val eventKey = event.key()) {
|
||||
is TimelineKey.TransactionId -> eventKey.txnId
|
||||
is TimelineKey.EventId -> eventKey.eventId
|
||||
}
|
||||
}
|
||||
|
||||
object Virtual : MatrixTimelineItem
|
||||
object Other : MatrixTimelineItem
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user