Timeline: refactor factory/cache
This commit is contained in:
parent
81d2ca02c2
commit
efa052a5ac
5 changed files with 122 additions and 74 deletions
|
|
@ -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)
|
||||||
|
|
||||||
suspend fun create(
|
fun flow(): StateFlow<List<MessagesTimelineItemState>> = timelineItemStates.asStateFlow()
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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…
Add table
Add a link
Reference in a new issue