From 81d2ca02c2ddc5a27e2f5caa35eb588ef7bb59a2 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 6 Dec 2022 13:17:33 +0100 Subject: [PATCH] Timeline: first version of diff/cache --- features/messages/build.gradle.kts | 1 + .../MessageTimelineItemStateFactory.kt | 99 ++++++++++++++----- .../messages/diff/CacheInvalidator.kt | 38 +++++++ .../diff/MatrixTimelineItemsDiffCallback.kt | 35 +++++++ gradle/libs.versions.toml | 2 + .../x/matrix/timeline/MatrixTimelineItem.kt | 10 +- 6 files changed, 157 insertions(+), 28 deletions(-) create mode 100644 features/messages/src/main/java/io/element/android/x/features/messages/diff/CacheInvalidator.kt create mode 100644 features/messages/src/main/java/io/element/android/x/features/messages/diff/MatrixTimelineItemsDiffCallback.kt diff --git a/features/messages/build.gradle.kts b/features/messages/build.gradle.kts index 39cdccb043..43c850e91c 100644 --- a/features/messages/build.gradle.kts +++ b/features/messages/build.gradle.kts @@ -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") diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/MessageTimelineItemStateFactory.kt b/features/messages/src/main/java/io/element/android/x/features/messages/MessageTimelineItemStateFactory.kt index 61a3a40978..fab9a0b012 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/MessageTimelineItemStateFactory.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/MessageTimelineItemStateFactory.kt @@ -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() + private var currentSnapshot: List = emptyList() + + private val lock = Mutex() + private val cacheInvalidator = CacheInvalidator(timelineItemCaches) + suspend fun create( timelineItems: List, ): List = withContext(dispatcher) { - val messagesTimelineItemState = ArrayList() - 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): List { + val messagesTimelineItemState = ArrayList() + 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) { + 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, + 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) } - - -} \ No newline at end of file +} diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/diff/CacheInvalidator.kt b/features/messages/src/main/java/io/element/android/x/features/messages/diff/CacheInvalidator.kt new file mode 100644 index 0000000000..a0a68e2912 --- /dev/null +++ b/features/messages/src/main/java/io/element/android/x/features/messages/diff/CacheInvalidator.kt @@ -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) : + 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) + } + } + +} diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/diff/MatrixTimelineItemsDiffCallback.kt b/features/messages/src/main/java/io/element/android/x/features/messages/diff/MatrixTimelineItemsDiffCallback.kt new file mode 100644 index 0000000000..92048d670b --- /dev/null +++ b/features/messages/src/main/java/io/element/android/x/features/messages/diff/MatrixTimelineItemsDiffCallback.kt @@ -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, + private val newList: List +) : 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 + } + +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8b3eec9929..a01d057e24 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/MatrixTimelineItem.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/MatrixTimelineItem.kt index ff9695d428..c16619b80f 100644 --- a/libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/MatrixTimelineItem.kt +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/MatrixTimelineItem.kt @@ -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 }