Timeline: first version of diff/cache

This commit is contained in:
ganfra
2022-12-06 13:17:33 +01:00
parent e10c94b654
commit 81d2ca02c2
6 changed files with 157 additions and 28 deletions

View File

@@ -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")

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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
}
}

View File

@@ -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" }

View File

@@ -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
}