Browse Source

Timeline: refactor factory/cache

feature/bma/flipper
ganfra 2 years ago
parent
commit
cd0923c31e
  1. 68
      features/messages/src/main/java/io/element/android/x/features/messages/MessageTimelineItemStateFactory.kt
  2. 96
      features/messages/src/main/java/io/element/android/x/features/messages/MessagesViewModel.kt
  3. 15
      features/messages/src/main/java/io/element/android/x/features/messages/diff/CacheInvalidator.kt
  4. 8
      features/messages/src/main/java/io/element/android/x/features/messages/util/MutableListExt.kt
  5. 9
      libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/MatrixTimeline.kt

68
features/messages/src/main/java/io/element/android/x/features/messages/MessageTimelineItemStateFactory.kt

@ -10,11 +10,15 @@ 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
import io.element.android.x.features.messages.model.MessagesTimelineItemState import io.element.android.x.features.messages.model.MessagesTimelineItemState
import io.element.android.x.features.messages.model.content.* import io.element.android.x.features.messages.model.content.*
import io.element.android.x.features.messages.util.invalidateLast
import io.element.android.x.matrix.MatrixClient import io.element.android.x.matrix.MatrixClient
import io.element.android.x.matrix.media.MediaResolver 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.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -32,51 +36,66 @@ class MessageTimelineItemStateFactory(
private val dispatcher: CoroutineDispatcher, private val dispatcher: CoroutineDispatcher,
) { ) {
private val timelineItemCaches = arrayListOf<MessagesTimelineItemState?>() private val timelineItemStates = MutableStateFlow<List<MessagesTimelineItemState>>(emptyList())
private var currentSnapshot: List<MatrixTimelineItem> = emptyList() private val timelineItemStatesCache = arrayListOf<MessagesTimelineItemState?>()
// Items from rust sdk, used for diffing
private var timelineItems: List<MatrixTimelineItem> = emptyList()
private val lock = Mutex() private val lock = Mutex()
private val cacheInvalidator = CacheInvalidator(timelineItemCaches) private val cacheInvalidator = CacheInvalidator(timelineItemStatesCache)
fun flow(): StateFlow<List<MessagesTimelineItemState>> = timelineItemStates.asStateFlow()
suspend fun create( suspend fun replaceWith(
timelineItems: List<MatrixTimelineItem>, timelineItems: List<MatrixTimelineItem>,
): List<MessagesTimelineItemState> = ) = withContext(dispatcher) {
withContext(dispatcher) { lock.withLock {
lock.withLock { calculateAndApplyDiff(timelineItems)
calculateAndApplyDiff(timelineItems) buildAndEmitTimelineItemStates(timelineItems)
getOrCreateFromCache(timelineItems)
}
} }
}
private suspend fun getOrCreateFromCache(timelineItems: List<MatrixTimelineItem>): List<MessagesTimelineItemState> { suspend fun pushItem(
val messagesTimelineItemState = ArrayList<MessagesTimelineItemState>() timelineItem: MatrixTimelineItem,
for (index in timelineItemCaches.indices.reversed()) { ) = withContext(dispatcher) {
val cacheItem = timelineItemCaches[index] lock.withLock {
// Makes sure to invalidate last as we need to recompute some data (like groupPosition)
timelineItemStatesCache.invalidateLast()
timelineItemStatesCache.add(null)
timelineItems = timelineItems + timelineItem
buildAndEmitTimelineItemStates(timelineItems)
}
}
private suspend fun buildAndEmitTimelineItemStates(timelineItems: List<MatrixTimelineItem>) {
val newTimelineItemStates = ArrayList<MessagesTimelineItemState>()
for (index in timelineItemStatesCache.indices.reversed()) {
val cacheItem = timelineItemStatesCache[index]
if (cacheItem == null) { if (cacheItem == null) {
buildAndCacheItem(timelineItems, index)?.also { timelineItemState -> buildAndCacheItem(timelineItems, index)?.also { timelineItemState ->
messagesTimelineItemState.add(timelineItemState) newTimelineItemStates.add(timelineItemState)
} }
} else { } else {
messagesTimelineItemState.add(cacheItem) newTimelineItemStates.add(cacheItem)
} }
} }
return messagesTimelineItemState timelineItemStates.emit(newTimelineItemStates)
} }
private fun calculateAndApplyDiff(timelineItems: List<MatrixTimelineItem>) { private fun calculateAndApplyDiff(newTimelineItems: List<MatrixTimelineItem>) {
val timeToDiff = measureTimeMillis { val timeToDiff = measureTimeMillis {
val diffCallback = val diffCallback =
MatrixTimelineItemsDiffCallback( MatrixTimelineItemsDiffCallback(
oldList = currentSnapshot, oldList = timelineItems,
newList = timelineItems newList = newTimelineItems
) )
val diffResult = DiffUtil.calculateDiff(diffCallback, false) val diffResult = DiffUtil.calculateDiff(diffCallback, false)
currentSnapshot = timelineItems timelineItems = newTimelineItems
diffResult.dispatchUpdatesTo(cacheInvalidator) diffResult.dispatchUpdatesTo(cacheInvalidator)
} }
Timber.v("Time to apply diff on new list of ${timelineItems.size} items: $timeToDiff ms") Timber.v("Time to apply diff on new list of ${newTimelineItems.size} items: $timeToDiff ms")
} }
private suspend fun buildAndCacheItem( private suspend fun buildAndCacheItem(
@ -97,7 +116,7 @@ class MessageTimelineItemStateFactory(
) )
MatrixTimelineItem.Other -> null MatrixTimelineItem.Other -> null
} }
timelineItemCaches[index] = timelineItemState timelineItemStatesCache[index] = timelineItemState
return timelineItemState return timelineItemState
} }
@ -213,4 +232,5 @@ 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)
} }
} }

96
features/messages/src/main/java/io/element/android/x/features/messages/MessagesViewModel.kt

@ -17,13 +17,12 @@ import io.element.android.x.matrix.MatrixInstance
import io.element.android.x.matrix.media.MediaResolver 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.MatrixTimeline import io.element.android.x.matrix.timeline.MatrixTimeline
import io.element.android.x.matrix.timeline.MatrixTimelineItem
import io.element.android.x.textcomposer.MessageComposerMode import io.element.android.x.textcomposer.MessageComposerMode
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
private const val PAGINATION_COUNT = 50 private const val PAGINATION_COUNT = 50
@ -57,6 +56,14 @@ class MessagesViewModel(
} }
} }
private val timelineCallback = object : MatrixTimeline.Callback {
override fun onPushedTimelineItem(timelineItem: MatrixTimelineItem) {
viewModelScope.launch {
messageTimelineItemStateFactory.pushItem(timelineItem)
}
}
}
init { init {
handleInit() handleInit()
} }
@ -93,7 +100,10 @@ class MessagesViewModel(
return currentState.itemActionsSheetState.invoke()?.targetItem return currentState.itemActionsSheetState.invoke()?.targetItem
} }
fun handleItemAction(action: MessagesItemAction, targetEvent: MessagesTimelineItemState.MessageEvent) { fun handleItemAction(
action: MessagesItemAction,
targetEvent: MessagesTimelineItemState.MessageEvent
) {
viewModelScope.launch(Dispatchers.Default) { viewModelScope.launch(Dispatchers.Default) {
when (action) { when (action) {
MessagesItemAction.Copy -> notImplementedYet() MessagesItemAction.Copy -> notImplementedYet()
@ -109,46 +119,10 @@ class MessagesViewModel(
setComposerMode(MessageComposerMode.Normal("")) setComposerMode(MessageComposerMode.Normal(""))
} }
private fun handleActionEdit(targetEvent: MessagesTimelineItemState.MessageEvent) {
setComposerMode(
MessageComposerMode.Edit(
targetEvent.id,
(targetEvent.content as? MessagesTimelineItemTextBasedContent)?.body.orEmpty()
)
)
}
private fun handleActionReply(targetEvent: MessagesTimelineItemState.MessageEvent) {
setComposerMode(MessageComposerMode.Reply(targetEvent.safeSenderName, targetEvent.id, ""))
}
private fun setComposerMode(mode: MessageComposerMode) {
setState {
copy(
composerMode = mode,
highlightedEventId = mode.relatedEventId
)
}
}
private fun notImplementedYet() {
setSnackbarContent("Not implemented yet!")
}
fun onSnackbarShown() { fun onSnackbarShown() {
setSnackbarContent(null) setSnackbarContent(null)
} }
private fun setSnackbarContent(message: String?) {
setState { copy(snackbarContent = message) }
}
private fun handleActionRedact(event: MessagesTimelineItemState.MessageEvent) {
viewModelScope.launch {
room.redactEvent(event.id)
}
}
fun computeActionsSheetState(messagesTimelineItemState: MessagesTimelineItemState.MessageEvent?) { fun computeActionsSheetState(messagesTimelineItemState: MessagesTimelineItemState.MessageEvent?) {
if (messagesTimelineItemState == null) { if (messagesTimelineItemState == null) {
setState { copy(itemActionsSheetState = Uninitialized) } setState { copy(itemActionsSheetState = Uninitialized) }
@ -181,6 +155,7 @@ class MessagesViewModel(
private fun handleInit() { private fun handleInit() {
timeline.initialize() timeline.initialize()
timeline.callback = timelineCallback
room.syncUpdateFlow() room.syncUpdateFlow()
.onEach { .onEach {
val avatarData = val avatarData =
@ -194,12 +169,52 @@ class MessagesViewModel(
timeline timeline
.timelineItems() .timelineItems()
.map(messageTimelineItemStateFactory::create) .onEach(messageTimelineItemStateFactory::replaceWith)
.launchIn(viewModelScope)
messageTimelineItemStateFactory
.flow()
.execute { .execute {
copy(timelineItems = it) copy(timelineItems = it)
} }
} }
private fun setSnackbarContent(message: String?) {
setState { copy(snackbarContent = message) }
}
private fun handleActionRedact(event: MessagesTimelineItemState.MessageEvent) {
viewModelScope.launch {
room.redactEvent(event.id)
}
}
private fun handleActionEdit(targetEvent: MessagesTimelineItemState.MessageEvent) {
setComposerMode(
MessageComposerMode.Edit(
targetEvent.id,
(targetEvent.content as? MessagesTimelineItemTextBasedContent)?.body.orEmpty()
)
)
}
private fun handleActionReply(targetEvent: MessagesTimelineItemState.MessageEvent) {
setComposerMode(MessageComposerMode.Reply(targetEvent.safeSenderName, targetEvent.id, ""))
}
private fun setComposerMode(mode: MessageComposerMode) {
setState {
copy(
composerMode = mode,
highlightedEventId = mode.relatedEventId
)
}
}
private fun notImplementedYet() {
setSnackbarContent("Not implemented yet!")
}
private suspend fun loadAvatarData( private suspend fun loadAvatarData(
name: String, name: String,
url: String?, url: String?,
@ -212,6 +227,7 @@ class MessagesViewModel(
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
timeline.callback = null
timeline.dispose() timeline.dispose()
} }
} }

15
features/messages/src/main/java/io/element/android/x/features/messages/diff/CacheInvalidator.kt

@ -2,36 +2,39 @@ package io.element.android.x.features.messages.diff
import androidx.recyclerview.widget.ListUpdateCallback import androidx.recyclerview.widget.ListUpdateCallback
import io.element.android.x.features.messages.model.MessagesTimelineItemState import io.element.android.x.features.messages.model.MessagesTimelineItemState
import io.element.android.x.features.messages.util.invalidateLast
import timber.log.Timber import timber.log.Timber
internal class CacheInvalidator(private val timelineItemCache: MutableList<MessagesTimelineItemState?>) : internal class CacheInvalidator(private val itemStatesCache: MutableList<MessagesTimelineItemState?>) :
ListUpdateCallback { ListUpdateCallback {
override fun onChanged(position: Int, count: Int, payload: Any?) { override fun onChanged(position: Int, count: Int, payload: Any?) {
Timber.v("onChanged(position= $position, count= $count") Timber.v("onChanged(position= $position, count= $count")
(position until position + count).forEach { (position until position + count).forEach {
// Invalidate cache // Invalidate cache
timelineItemCache[it] = null itemStatesCache[it] = null
} }
} }
override fun onMoved(fromPosition: Int, toPosition: Int) { override fun onMoved(fromPosition: Int, toPosition: Int) {
Timber.v("onMoved(fromPosition= $fromPosition, toPosition= $toPosition") Timber.v("onMoved(fromPosition= $fromPosition, toPosition= $toPosition")
val model = timelineItemCache.removeAt(fromPosition) val model = itemStatesCache.removeAt(fromPosition)
timelineItemCache.add(toPosition, model) itemStatesCache.add(toPosition, model)
} }
override fun onInserted(position: Int, count: Int) { override fun onInserted(position: Int, count: Int) {
Timber.v("onInserted(position= $position, count= $count") Timber.v("onInserted(position= $position, count= $count")
itemStatesCache.invalidateLast()
repeat(count) { repeat(count) {
timelineItemCache.add(position, null) itemStatesCache.add(position, null)
} }
} }
override fun onRemoved(position: Int, count: Int) { override fun onRemoved(position: Int, count: Int) {
Timber.v("onRemoved(position= $position, count= $count") Timber.v("onRemoved(position= $position, count= $count")
itemStatesCache.invalidateLast()
repeat(count) { repeat(count) {
timelineItemCache.removeAt(position) itemStatesCache.removeAt(position)
} }
} }

8
features/messages/src/main/java/io/element/android/x/features/messages/util/MutableListExt.kt

@ -0,0 +1,8 @@
package io.element.android.x.features.messages.util
internal inline fun <reified T> MutableList<T?>.invalidateLast() {
val indexOfLast = size
if (indexOfLast > 0) {
set(indexOfLast - 1, null)
}
}

9
libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/MatrixTimeline.kt

@ -1,7 +1,6 @@
package io.element.android.x.matrix.timeline package io.element.android.x.matrix.timeline
import io.element.android.x.core.coroutine.CoroutineDispatchers import io.element.android.x.core.coroutine.CoroutineDispatchers
import io.element.android.x.matrix.core.EventId
import io.element.android.x.matrix.room.MatrixRoom import io.element.android.x.matrix.room.MatrixRoom
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -20,10 +19,10 @@ class MatrixTimeline(
private val coroutineScope: CoroutineScope, private val coroutineScope: CoroutineScope,
private val coroutineDispatchers: CoroutineDispatchers, private val coroutineDispatchers: CoroutineDispatchers,
) : TimelineListener { ) : TimelineListener {
interface Callback { interface Callback {
fun onUpdatedTimelineItem(eventId: EventId) fun onUpdatedTimelineItem(timelineItem: MatrixTimelineItem) = Unit
fun onStartedBackPaginating() fun onPushedTimelineItem(timelineItem: MatrixTimelineItem) = Unit
fun onFinishedBackPaginating()
} }
var callback: Callback? = null var callback: Callback? = null
@ -48,12 +47,14 @@ class MatrixTimeline(
TimelineChange.PUSH -> { TimelineChange.PUSH -> {
Timber.v("Apply push on list with size: $size") Timber.v("Apply push on list with size: $size")
val item = diff.push()?.asMatrixTimelineItem() ?: return val item = diff.push()?.asMatrixTimelineItem() ?: return
callback?.onPushedTimelineItem(item)
add(item) add(item)
} }
TimelineChange.UPDATE_AT -> { TimelineChange.UPDATE_AT -> {
val updateAtData = diff.updateAt() ?: return val updateAtData = diff.updateAt() ?: return
Timber.v("Apply $updateAtData on list with size: $size") Timber.v("Apply $updateAtData on list with size: $size")
val item = updateAtData.item.asMatrixTimelineItem() val item = updateAtData.item.asMatrixTimelineItem()
callback?.onUpdatedTimelineItem(item)
set(updateAtData.index.toInt(), item) set(updateAtData.index.toInt(), item)
} }
TimelineChange.INSERT_AT -> { TimelineChange.INSERT_AT -> {

Loading…
Cancel
Save