Timeline: first version of diff/cache
This commit is contained in:
parent
e10c94b654
commit
81d2ca02c2
6 changed files with 157 additions and 28 deletions
|
|
@ -18,6 +18,7 @@ dependencies {
|
||||||
implementation(libs.timber)
|
implementation(libs.timber)
|
||||||
implementation(libs.datetime)
|
implementation(libs.datetime)
|
||||||
implementation(libs.accompanist.flowlayout)
|
implementation(libs.accompanist.flowlayout)
|
||||||
|
implementation(libs.androidx.recyclerview)
|
||||||
implementation("org.jsoup:jsoup:1.15.3")
|
implementation("org.jsoup:jsoup:1.15.3")
|
||||||
testImplementation("junit:junit:4.13.2")
|
testImplementation("junit:junit:4.13.2")
|
||||||
androidTestImplementation("androidx.test.ext:junit:1.1.3")
|
androidTestImplementation("androidx.test.ext:junit:1.1.3")
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
package io.element.android.x.features.messages
|
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.AvatarData
|
||||||
import io.element.android.x.designsystem.components.avatar.AvatarSize
|
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.AggregatedReaction
|
||||||
import io.element.android.x.features.messages.model.MessagesItemGroupPosition
|
import io.element.android.x.features.messages.model.MessagesItemGroupPosition
|
||||||
import io.element.android.x.features.messages.model.MessagesItemReactionState
|
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.room.MatrixRoom
|
||||||
import io.element.android.x.matrix.timeline.MatrixTimelineItem
|
import io.element.android.x.matrix.timeline.MatrixTimelineItem
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.matrix.rustcomponents.sdk.FormattedBody
|
import org.matrix.rustcomponents.sdk.FormattedBody
|
||||||
import org.matrix.rustcomponents.sdk.MessageFormat
|
import org.matrix.rustcomponents.sdk.MessageFormat
|
||||||
import org.matrix.rustcomponents.sdk.MessageType
|
import org.matrix.rustcomponents.sdk.MessageType
|
||||||
import org.matrix.rustcomponents.sdk.TimelineKey
|
import timber.log.Timber
|
||||||
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
class MessageTimelineItemStateFactory(
|
class MessageTimelineItemStateFactory(
|
||||||
private val client: MatrixClient,
|
private val client: MatrixClient,
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
private val dispatcher: CoroutineDispatcher,
|
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(
|
suspend fun create(
|
||||||
timelineItems: List<MatrixTimelineItem>,
|
timelineItems: List<MatrixTimelineItem>,
|
||||||
): List<MessagesTimelineItemState> =
|
): List<MessagesTimelineItemState> =
|
||||||
withContext(dispatcher) {
|
withContext(dispatcher) {
|
||||||
val messagesTimelineItemState = ArrayList<MessagesTimelineItemState>()
|
lock.withLock {
|
||||||
for (index in timelineItems.indices.reversed()) {
|
calculateAndApplyDiff(timelineItems)
|
||||||
val currentTimelineItem = timelineItems[index]
|
getOrCreateFromCache(timelineItems)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
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(
|
private suspend fun buildMessageEvent(
|
||||||
currentTimelineItem: MatrixTimelineItem.Event,
|
currentTimelineItem: MatrixTimelineItem.Event,
|
||||||
index: Int,
|
index: Int,
|
||||||
|
|
@ -62,12 +113,8 @@ class MessageTimelineItemStateFactory(
|
||||||
val senderAvatarUrl = room.userAvatarUrl(currentSender).getOrNull()
|
val senderAvatarUrl = room.userAvatarUrl(currentSender).getOrNull()
|
||||||
val senderAvatarData =
|
val senderAvatarData =
|
||||||
loadAvatarData(senderDisplayName ?: currentSender, senderAvatarUrl)
|
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(
|
return MessagesTimelineItemState.MessageEvent(
|
||||||
id = uniqueId,
|
id = currentTimelineItem.uniqueId,
|
||||||
senderId = currentSender,
|
senderId = currentSender,
|
||||||
senderDisplayName = senderDisplayName,
|
senderDisplayName = senderDisplayName,
|
||||||
senderAvatar = senderAvatarData,
|
senderAvatar = senderAvatarData,
|
||||||
|
|
@ -166,6 +213,4 @@ class MessageTimelineItemStateFactory(
|
||||||
.resolve(url, kind = MediaResolver.Kind.Thumbnail(size.value))
|
.resolve(url, kind = MediaResolver.Kind.Thumbnail(size.value))
|
||||||
return AvatarData(name, model, size)
|
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"
|
corektx = "1.9.0"
|
||||||
datastore = "1.0.0"
|
datastore = "1.0.0"
|
||||||
constraintlayout = "2.1.4"
|
constraintlayout = "2.1.4"
|
||||||
|
recyclerview = "1.2.1"
|
||||||
|
|
||||||
# Compose
|
# Compose
|
||||||
compose_compiler = "1.3.2"
|
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_preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" }
|
||||||
androidx_datastore_datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" }
|
androidx_datastore_datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" }
|
||||||
androidx_constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" }
|
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_bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose_bom" }
|
||||||
androidx_compose_foundation = { group = "androidx.compose.foundation", name = "foundation" }
|
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.EventTimelineItem
|
||||||
import org.matrix.rustcomponents.sdk.TimelineItem
|
import org.matrix.rustcomponents.sdk.TimelineItem
|
||||||
|
import org.matrix.rustcomponents.sdk.TimelineKey
|
||||||
|
|
||||||
sealed interface MatrixTimelineItem {
|
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 Virtual : MatrixTimelineItem
|
||||||
object Other : MatrixTimelineItem
|
object Other : MatrixTimelineItem
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue