Timeline : try to get a better PagedList/Epoxy integration. Still need to be refined.

This commit is contained in:
ganfra 2019-01-10 11:37:14 +01:00 committed by ganfra
parent de90cbe73e
commit 922609cb57
14 changed files with 381 additions and 98 deletions

View File

@ -56,6 +56,8 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
// Paging
implementation "android.arch.paging:runtime:1.0.1"

implementation 'com.jakewharton.threetenabp:threetenabp:1.1.1'
implementation 'com.jakewharton.timber:timber:4.7.1'
@ -66,7 +68,6 @@ dependencies {

implementation("com.airbnb.android:epoxy:$epoxy_version")
kapt "com.airbnb.android:epoxy-processor:$epoxy_version"
implementation "com.airbnb.android:epoxy-paging:$epoxy_version"
implementation 'com.airbnb.android:mvrx:0.6.0'

// FP

View File

@ -73,9 +73,8 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
val layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true)
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager)
recyclerView.layoutManager = layoutManager
//timelineEventController.addModelBuildListener { it.dispatchTo(scrollOnNewMessageCallback) }
recyclerView.setHasFixedSize(true)
recyclerView.setItemViewCacheSize(20)
//timelineEventController.addModelBuildListener { it.dispatchTo(scrollOnNewMessageCallback) }
recyclerView.setController(timelineEventController)
timelineEventController.callback = this
}

View File

@ -8,7 +8,7 @@ import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.KotlinModel
import im.vector.riotredesign.features.home.AvatarRenderer

data class MessageItem(
class MessageItem(
val message: CharSequence? = null,
val time: CharSequence? = null,
val avatarUrl: String?,

View File

@ -4,7 +4,7 @@ import android.widget.TextView
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.KotlinModel

data class TextItem(
class TextItem(
val text: CharSequence? = null
) : KotlinModel(R.layout.item_event_text) {


View File

@ -1,106 +1,80 @@
package im.vector.riotredesign.features.home.room.detail.timeline

import android.arch.paging.PagedList
import com.airbnb.epoxy.EpoxyAsyncUtil
import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.EpoxyModel
import im.vector.matrix.android.api.session.events.model.EnrichedEvent
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.timeline.TimelineData
import im.vector.riotredesign.core.extensions.localDateTime
import im.vector.riotredesign.features.home.LoadingItemModel_
import im.vector.riotredesign.features.home.room.detail.timeline.paging.PagedListEpoxyController

class TimelineEventController(private val roomId: String,
private val messageItemFactory: MessageItemFactory,
private val textItemFactory: TextItemFactory,
private val dateFormatter: TimelineDateFormatter
) : EpoxyController(
) : PagedListEpoxyController<EnrichedEvent>(
EpoxyAsyncUtil.getAsyncBackgroundHandler(),
EpoxyAsyncUtil.getAsyncBackgroundHandler()
) {

init {
setFilterDuplicates(true)
}

private val pagedListCallback = object : PagedList.Callback() {
override fun onChanged(position: Int, count: Int) {
buildSnapshotList()
}
private var isLoadingForward: Boolean = false
private var isLoadingBackward: Boolean = false

override fun onInserted(position: Int, count: Int) {
buildSnapshotList()
}

override fun onRemoved(position: Int, count: Int) {
buildSnapshotList()
}
}

private var snapshotList: List<EnrichedEvent> = emptyList()
private var timelineData: TimelineData? = null
var callback: Callback? = null

fun update(timelineData: TimelineData?) {
timelineData?.events?.removeWeakCallback(pagedListCallback)
this.timelineData = timelineData
timelineData?.events?.addWeakCallback(null, pagedListCallback)
buildSnapshotList()
isLoadingForward = timelineData?.isLoadingForward ?: false
isLoadingBackward = timelineData?.isLoadingBackward ?: false
submitList(timelineData?.events)
requestModelBuild()
}

override fun buildModels() {
buildModelsWith(
snapshotList,
timelineData?.isLoadingForward ?: false,
timelineData?.isLoadingBackward ?: false
)
}

private fun buildModelsWith(events: List<EnrichedEvent?>,
isLoadingForward: Boolean,
isLoadingBackward: Boolean) {
if (events.isEmpty()) {
return
override fun buildItemModels(currentPosition: Int, items: List<EnrichedEvent?>): List<EpoxyModel<*>> {
if (items.isNullOrEmpty()) {
return emptyList()
}
val epoxyModels = ArrayList<EpoxyModel<*>>()
val event = items[currentPosition] ?: return emptyList()
val nextEvent = if (currentPosition + 1 < items.size) items[currentPosition + 1] else null

val date = event.root.localDateTime()
val nextDate = nextEvent?.root?.localDateTime()
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()

val item = when (event.root.type) {
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, addDaySeparator, date, callback)
else -> textItemFactory.create(event)
}
item?.also {
it.id(event.localId)
epoxyModels.add(it)
}

if (addDaySeparator) {
val formattedDay = dateFormatter.formatMessageDay(date)
val daySeparatorItem = DaySeparatorItem(formattedDay).id(roomId + formattedDay)
epoxyModels.add(daySeparatorItem)
}
return epoxyModels
}

override fun addModels(models: List<EpoxyModel<*>>) {
LoadingItemModel_()
.id(roomId + "forward_loading_item")
.addIf(isLoadingForward, this)

for (index in 0 until events.size) {
val event = events[index] ?: continue
val nextEvent = if (index + 1 < events.size) events[index + 1] else null

val date = event.root.localDateTime()
val nextDate = nextEvent?.root?.localDateTime()
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()

val item = when (event.root.type) {
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, addDaySeparator, date, callback)
else -> textItemFactory.create(event)
}

item
?.onBind {
timelineData?.events?.loadAround(index)
}
?.id(event.localId)
?.addTo(this)

if (addDaySeparator) {
val formattedDay = dateFormatter.formatMessageDay(date)
DaySeparatorItem(formattedDay).id(roomId + formattedDay).addTo(this)
}
}
super.add(models)

LoadingItemModel_()
.id(roomId + "backward_loading_item")
.addIf(isLoadingBackward, this)

}

private fun buildSnapshotList() {
snapshotList = timelineData?.events?.snapshot() ?: emptyList()
requestModelBuild()
}

interface Callback {
fun onUrlClicked(url: String)

View File

@ -0,0 +1,125 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.vector.riotredesign.features.home.room.detail.timeline.paging

import android.arch.paging.PagedList
import android.os.Handler
import android.support.v7.util.DiffUtil
import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.EpoxyViewHolder

/**
* An [EpoxyController] that can work with a [PagedList].
*
* Internally, it caches the model for each item in the [PagedList]. You should override
* [buildItemModel] method to build the model for the given item. Since [PagedList] might include
* `null` items if placeholders are enabled, this method needs to handle `null` values in the list.
*
* By default, the model for each item is added to the model list. To change this behavior (to
* filter items or inject extra items), you can override [addModels] function and manually add built
* models.
*
* @param T The type of the items in the [PagedList].
*/
abstract class PagedListEpoxyController<T>(
/**
* The handler to use for building models. By default this uses the main thread, but you can use
* [EpoxyAsyncUtil.getAsyncBackgroundHandler] to do model building in the background.
*
* The notify thread of your PagedList (from setNotifyExecutor in the PagedList Builder) must be
* the same as this thread. Otherwise Epoxy will crash.
*/
modelBuildingHandler: Handler = EpoxyController.defaultModelBuildingHandler,
/**
* The handler to use when calculating the diff between built model lists.
* By default this uses the main thread, but you can use
* [EpoxyAsyncUtil.getAsyncBackgroundHandler] to do diffing in the background.
*/
diffingHandler: Handler = EpoxyController.defaultDiffingHandler,
/**
* [PagedListEpoxyController] uses an [DiffUtil.ItemCallback] to detect changes between
* [PagedList]s. By default, it relies on simple object equality but you can provide a custom
* one if you don't use all fields in the object in your models.
*/
itemDiffCallback: DiffUtil.ItemCallback<T> = DEFAULT_ITEM_DIFF_CALLBACK as DiffUtil.ItemCallback<T>
) : EpoxyController(modelBuildingHandler, diffingHandler) {
// this is where we keep the already built models
protected val modelCache = PagedListModelCache(
modelBuilder = { pos, item ->
buildItemModels(pos, item)
},
rebuildCallback = {
requestModelBuild()
},
itemDiffCallback = itemDiffCallback,
modelBuildingHandler = modelBuildingHandler
)

final override fun buildModels() {
addModels(modelCache.getModels())
}

override fun onModelBound(
holder: EpoxyViewHolder,
boundModel: EpoxyModel<*>,
position: Int,
previouslyBoundModel: EpoxyModel<*>?
) {
modelCache.loadAround(boundModel)
}

/**
* This function adds all built models to the adapter. You can override this method to add extra
* items into the model list or remove some.
*/
open fun addModels(models: List<EpoxyModel<*>>) {
super.add(models)
}

/**
* Builds the model for a given item. This must return a single model for each item. If you want
* to inject headers etc, you can override [addModels] function.
*
* If the `item` is `null`, you should provide the placeholder. If your [PagedList] is configured
* without placeholders, you don't need to handle the `null` case.
*/
abstract fun buildItemModels(currentPosition: Int, items: List<T?>): List<EpoxyModel<*>>

/**
* Submit a new paged list.
*
* A diff will be calculated between this list and the previous list so you may still get calls
* to [buildItemModel] with items from the previous list.
*/
fun submitList(newList: PagedList<T>?) {
modelCache.submitList(newList)
}

companion object {
/**
* [PagedListEpoxyController] calculates a diff on top of the PagedList to check which
* models are invalidated.
* This is the default [DiffUtil.ItemCallback] which uses object equality.
*/
val DEFAULT_ITEM_DIFF_CALLBACK = object : DiffUtil.ItemCallback<Any>() {
override fun areItemsTheSame(oldItem: Any, newItem: Any) = oldItem == newItem

override fun areContentsTheSame(oldItem: Any, newItem: Any) = oldItem == newItem
}
}
}

View File

@ -0,0 +1,171 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.vector.riotredesign.features.home.room.detail.timeline.paging

import android.annotation.SuppressLint
import android.arch.paging.AsyncPagedListDiffer
import android.arch.paging.PagedList
import android.os.Handler
import android.support.v7.recyclerview.extensions.AsyncDifferConfig
import android.support.v7.util.DiffUtil
import android.support.v7.util.ListUpdateCallback
import android.util.Log
import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.EpoxyModel
import java.util.concurrent.Executor
import java.util.concurrent.atomic.AtomicBoolean

/**
* A PagedList stream wrapper that caches models built for each item. It tracks changes in paged lists and caches
* models for each item when they are invalidated to avoid rebuilding models for the whole list when PagedList is
* updated.
*/
class PagedListModelCache<T>(
private val modelBuilder: (itemIndex: Int, items: List<T>) -> List<EpoxyModel<*>>,
private val rebuildCallback: () -> Unit,
private val itemDiffCallback: DiffUtil.ItemCallback<T>,
private val diffExecutor: Executor? = null,
private val modelBuildingHandler: Handler
) {


// Int is the index of the pagedList item
// We have to be able to find the pagedlist position coming from an epoxy model to trigger
// LoadAround with accuracy
private val modelCache = linkedMapOf<EpoxyModel<*>, Int>()
private var isCacheStale = AtomicBoolean(true)

/**
* Tracks the last accessed position so that we can report it back to the paged list when models are built.
*/
private var lastPosition: Int? = null

/**
* Observer for the PagedList changes that invalidates the model cache when data is updated.
*/
private val updateCallback = object : ListUpdateCallback {
override fun onChanged(position: Int, count: Int, payload: Any?) {
invalidate()
rebuildCallback()
}

override fun onMoved(fromPosition: Int, toPosition: Int) {
invalidate()
rebuildCallback()
}

override fun onInserted(position: Int, count: Int) {
invalidate()
rebuildCallback()
}

override fun onRemoved(position: Int, count: Int) {
invalidate()
rebuildCallback()
}
}

private val asyncDiffer = @SuppressLint("RestrictedApi")
object : AsyncPagedListDiffer<T>(
updateCallback,
AsyncDifferConfig.Builder<T>(
itemDiffCallback
).also { builder ->
if (diffExecutor != null) {
builder.setBackgroundThreadExecutor(diffExecutor)
}
// we have to reply on this private API, otherwise, paged list might be changed when models are being built,
// potentially creating concurrent modification problems.
builder.setMainThreadExecutor { runnable: Runnable ->
modelBuildingHandler.post(runnable)
}
}.build()
) {
init {
if (modelBuildingHandler != EpoxyController.defaultModelBuildingHandler) {
try {
// looks like AsyncPagedListDiffer in 1.x ignores the config.
// Reflection to the rescue.
val mainThreadExecutorField =
AsyncPagedListDiffer::class.java.getDeclaredField("mMainThreadExecutor")
mainThreadExecutorField.isAccessible = true
mainThreadExecutorField.set(this, Executor {
modelBuildingHandler.post(it)
})
} catch (t: Throwable) {
val msg = "Failed to hijack update handler in AsyncPagedListDiffer." +
"You can only build models on the main thread"
Log.e("PagedListModelCache", msg, t)
throw IllegalStateException(msg, t)
}
}
}
}

fun submitList(pagedList: PagedList<T>?) {
asyncDiffer.submitList(pagedList)
}

fun getModels(): List<EpoxyModel<*>> {
if (isCacheStale.compareAndSet(true, false)) {
asyncDiffer.currentList?.forEachIndexed { position, _ ->
buildModel(position)
}
}
lastPosition?.let {
triggerLoadAround(it)
}
return modelCache.keys.toList()
}

fun loadAround(model: EpoxyModel<*>) {
modelCache[model]?.let { itemPosition ->
triggerLoadAround(itemPosition)
lastPosition = itemPosition
}
}

// PRIVATE METHODS *****************************************************************************

private fun invalidate() {
modelCache.clear()
isCacheStale.set(true)
}

private fun cacheModelsAtPosition(itemPosition: Int, epoxyModels: Set<EpoxyModel<*>>) {
epoxyModels.forEach {
modelCache[it] = itemPosition
}
}

private fun buildModel(pos: Int) {
if (pos >= asyncDiffer.currentList?.size ?: 0) {
return
}
modelBuilder(pos, asyncDiffer.currentList as List<T>).also {
cacheModelsAtPosition(pos, it.toSet())
}
}

private fun triggerLoadAround(position: Int) {
asyncDiffer.currentList?.let {
if (it.size > 0) {
it.loadAround(Math.min(position, it.size - 1))
}
}
}
}

View File

@ -28,7 +28,7 @@
android:ellipsize="end"
android:maxLines="1"
android:paddingBottom="8dp"
android:textSize="15sp"
android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@+id/toolbarSubtitleView"
app:layout_constraintEnd_toStartOf="@+id/messageTimeView"
app:layout_constraintHorizontal_bias="0.0"
@ -55,7 +55,7 @@
android:layout_marginStart="64dp"
android:layout_marginBottom="8dp"
android:textColor="@color/dark_grey"
android:textSize="14sp"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

View File

@ -37,6 +37,10 @@ internal fun ChunkEntity.Companion.findAllIncludingEvents(realm: Realm, eventIds
.findAll()
}

internal fun ChunkEntity.Companion.findIncludingEvent(realm: Realm, eventId: String): ChunkEntity? {
return findAllIncludingEvents(realm, listOf(eventId)).firstOrNull()
}

internal fun ChunkEntity.Companion.create(realm: Realm, prevToken: String?, nextToken: String?): ChunkEntity {
return realm.createObject<ChunkEntity>().apply {
this.prevToken = prevToken

View File

@ -15,7 +15,12 @@ import io.realm.RealmQuery
internal class RoomMemberExtractor(private val monarchy: Monarchy,
private val roomId: String) {

private val cached = HashMap<String, RoomMember?>()

fun extractFrom(event: EventEntity): RoomMember? {
if (cached.containsKey(event.eventId)) {
return cached[event.eventId]
}
val sender = event.sender ?: return null
// If the event is unlinked we want to fetch unlinked state events
val unlinked = event.isUnlinked
@ -23,11 +28,13 @@ internal class RoomMemberExtractor(private val monarchy: Monarchy,
// If prevContent is null we fallback to the Int.MIN state events content()
val content = if (event.stateIndex <= 0) {
baseQuery(monarchy, roomId, sender, unlinked).next(from = event.stateIndex)?.prevContent
?: baseQuery(monarchy, roomId, sender, unlinked).last(since = event.stateIndex)?.content
?: baseQuery(monarchy, roomId, sender, unlinked).last(since = event.stateIndex)?.content
} else {
baseQuery(monarchy, roomId, sender, unlinked).last(since = event.stateIndex)?.content
}
return ContentMapper.map(content).toModel()
val roomMember: RoomMember? = ContentMapper.map(content).toModel()
cached[event.eventId] = roomMember
return roomMember
}

private fun baseQuery(monarchy: Monarchy,

View File

@ -7,7 +7,7 @@ import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.FilterUtil


internal interface PaginationTask : Task<PaginationTask.Params, TokenChunkEvent> {
internal interface PaginationTask : Task<PaginationTask.Params, Boolean> {

data class Params(
val roomId: String,
@ -22,14 +22,13 @@ internal class DefaultPaginationTask(private val roomAPI: RoomAPI,
private val tokenChunkEventPersistor: TokenChunkEventPersistor
) : PaginationTask {

override fun execute(params: PaginationTask.Params): Try<TokenChunkEvent> {
override fun execute(params: PaginationTask.Params): Try<Boolean> {
val filter = FilterUtil.createRoomEventFilter(true)?.toJSONString()
return executeRequest<PaginationResponse> {
apiCall = roomAPI.getRoomMessagesFrom(params.roomId, params.from, params.direction.value, params.limit, filter)
}.flatMap { chunk ->
tokenChunkEventPersistor
.insertInDb(chunk, params.roomId, params.direction)
.map { chunk }
}
}


View File

@ -22,7 +22,8 @@ import im.vector.matrix.android.internal.util.tryTransactionAsync
import io.realm.Realm
import io.realm.RealmQuery

private const val PAGE_SIZE = 30
private const val PAGE_SIZE = 50
private const val PREFETCH_DISTANCE = 20

internal class DefaultTimelineService(private val roomId: String,
private val monarchy: Monarchy,
@ -51,6 +52,7 @@ internal class DefaultTimelineService(private val roomId: String,
val pagedListConfig = PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setPageSize(PAGE_SIZE)
.setPrefetchDistance(PREFETCH_DISTANCE)
.build()

val livePagedListBuilder = LivePagedListBuilder(domainSourceFactory, pagedListConfig).setBoundaryCallback(boundaryCallback)

View File

@ -6,11 +6,11 @@ import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.events.model.EnrichedEvent
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.query.findAllIncludingEvents
import im.vector.matrix.android.internal.database.query.findIncludingEvent
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.PagingRequestHelper
import java.util.*
import timber.log.Timber

internal class TimelineBoundaryCallback(private val roomId: String,
private val taskExecutor: TaskExecutor,
@ -43,8 +43,9 @@ internal class TimelineBoundaryCallback(private val roomId: String,
}

override fun onItemAtEndLoaded(itemAtEnd: EnrichedEvent) {
Timber.v("On item at end loaded")
val token = itemAtEnd.root.eventId?.let { getToken(it, PaginationDirection.BACKWARDS) }
?: return
?: return

helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER) {
runPaginationRequest(it, token, PaginationDirection.BACKWARDS)
@ -52,8 +53,9 @@ internal class TimelineBoundaryCallback(private val roomId: String,
}

override fun onItemAtFrontLoaded(itemAtFront: EnrichedEvent) {
Timber.v("On item at front loaded")
val token = itemAtFront.root.eventId?.let { getToken(it, PaginationDirection.FORWARDS) }
?: return
?: return

helper.runIfNotRunning(PagingRequestHelper.RequestType.BEFORE) {
runPaginationRequest(it, token, PaginationDirection.FORWARDS)
@ -63,7 +65,7 @@ internal class TimelineBoundaryCallback(private val roomId: String,
private fun getToken(eventId: String, direction: PaginationDirection): String? {
var token: String? = null
monarchy.doWithRealm { realm ->
val chunkEntity = ChunkEntity.findAllIncludingEvents(realm, Collections.singletonList(eventId)).firstOrNull()
val chunkEntity = ChunkEntity.findIncludingEvent(realm, eventId)
token = if (direction == PaginationDirection.FORWARDS) chunkEntity?.nextToken else chunkEntity?.prevToken
}
return token
@ -74,14 +76,14 @@ internal class TimelineBoundaryCallback(private val roomId: String,
direction: PaginationDirection) {

val params = PaginationTask.Params(roomId = roomId,
from = from,
direction = direction,
limit = limit)
from = from,
direction = direction,
limit = limit)

paginationTask.configureWith(params)
.enableRetry()
.dispatchTo(object : MatrixCallback<TokenChunkEvent> {
override fun onSuccess(data: TokenChunkEvent) {
.dispatchTo(object : MatrixCallback<Boolean> {
override fun onSuccess(data: Boolean) {
requestCallback.recordSuccess()
}


View File

@ -2,12 +2,7 @@ package im.vector.matrix.android.internal.session.room.timeline

import arrow.core.Try
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.internal.database.helper.addAll
import im.vector.matrix.android.internal.database.helper.addOrUpdate
import im.vector.matrix.android.internal.database.helper.addStateEvents
import im.vector.matrix.android.internal.database.helper.deleteOnCascade
import im.vector.matrix.android.internal.database.helper.isUnlinked
import im.vector.matrix.android.internal.database.helper.merge
import im.vector.matrix.android.internal.database.helper.*
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.query.create
@ -21,12 +16,15 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {

fun insertInDb(receivedChunk: TokenChunkEvent,
roomId: String,
direction: PaginationDirection): Try<Unit> {
direction: PaginationDirection): Try<Boolean> {

if (receivedChunk.events.isEmpty() && receivedChunk.stateEvents.isEmpty()) {
return Try.just(false)
}
return monarchy
.tryTransactionSync { realm ->
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: throw IllegalStateException("You shouldn't use this method without a room")
?: throw IllegalStateException("You shouldn't use this method without a room")

val nextToken: String?
val prevToken: String?
@ -46,10 +44,10 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {

var currentChunk = if (direction == PaginationDirection.FORWARDS) {
prevChunk?.apply { this.nextToken = nextToken }
?: ChunkEntity.create(realm, prevToken, nextToken)
?: ChunkEntity.create(realm, prevToken, nextToken)
} else {
nextChunk?.apply { this.prevToken = prevToken }
?: ChunkEntity.create(realm, prevToken, nextToken)
?: ChunkEntity.create(realm, prevToken, nextToken)
}

currentChunk.addAll(roomId, receivedChunk.events, direction, isUnlinked = currentChunk.isUnlinked())
@ -71,6 +69,7 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {
roomEntity.addOrUpdate(currentChunk)
roomEntity.addStateEvents(receivedChunk.stateEvents, isUnlinked = currentChunk.isUnlinked())
}
.map { true }
}

private fun handleMerge(roomEntity: RoomEntity,