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 "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:28.0.0' implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3' 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.threetenabp:threetenabp:1.1.1'
implementation 'com.jakewharton.timber:timber:4.7.1' implementation 'com.jakewharton.timber:timber:4.7.1'
@ -66,7 +68,6 @@ dependencies {


implementation("com.airbnb.android:epoxy:$epoxy_version") implementation("com.airbnb.android:epoxy:$epoxy_version")
kapt "com.airbnb.android:epoxy-processor:$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' implementation 'com.airbnb.android:mvrx:0.6.0'


// FP // FP

View File

@ -73,9 +73,8 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
val layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true) val layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true)
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager) scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager)
recyclerView.layoutManager = layoutManager recyclerView.layoutManager = layoutManager
//timelineEventController.addModelBuildListener { it.dispatchTo(scrollOnNewMessageCallback) }
recyclerView.setHasFixedSize(true) recyclerView.setHasFixedSize(true)
recyclerView.setItemViewCacheSize(20) //timelineEventController.addModelBuildListener { it.dispatchTo(scrollOnNewMessageCallback) }
recyclerView.setController(timelineEventController) recyclerView.setController(timelineEventController)
timelineEventController.callback = this 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.core.epoxy.KotlinModel
import im.vector.riotredesign.features.home.AvatarRenderer import im.vector.riotredesign.features.home.AvatarRenderer


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

View File

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


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



View File

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


import android.arch.paging.PagedList
import com.airbnb.epoxy.EpoxyAsyncUtil 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.EnrichedEvent
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.timeline.TimelineData import im.vector.matrix.android.api.session.room.timeline.TimelineData
import im.vector.riotredesign.core.extensions.localDateTime import im.vector.riotredesign.core.extensions.localDateTime
import im.vector.riotredesign.features.home.LoadingItemModel_ import im.vector.riotredesign.features.home.LoadingItemModel_
import im.vector.riotredesign.features.home.room.detail.timeline.paging.PagedListEpoxyController


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

init { init {
setFilterDuplicates(true) setFilterDuplicates(true)
} }


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


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 var callback: Callback? = null


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


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


private fun buildModelsWith(events: List<EnrichedEvent?>, override fun buildItemModels(currentPosition: Int, items: List<EnrichedEvent?>): List<EpoxyModel<*>> {
isLoadingForward: Boolean, if (items.isNullOrEmpty()) {
isLoadingBackward: Boolean) { return emptyList()
if (events.isEmpty()) {
return
} }
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_() LoadingItemModel_()
.id(roomId + "forward_loading_item") .id(roomId + "forward_loading_item")
.addIf(isLoadingForward, this) .addIf(isLoadingForward, this)


for (index in 0 until events.size) { super.add(models)
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)
}
}


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

} }


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


interface Callback { interface Callback {
fun onUrlClicked(url: String) 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:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:paddingBottom="8dp" android:paddingBottom="8dp"
android:textSize="15sp" android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@+id/toolbarSubtitleView" app:layout_constraintBottom_toTopOf="@+id/toolbarSubtitleView"
app:layout_constraintEnd_toStartOf="@+id/messageTimeView" app:layout_constraintEnd_toStartOf="@+id/messageTimeView"
app:layout_constraintHorizontal_bias="0.0" app:layout_constraintHorizontal_bias="0.0"
@ -55,7 +55,7 @@
android:layout_marginStart="64dp" android:layout_marginStart="64dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:textColor="@color/dark_grey" android:textColor="@color/dark_grey"
android:textSize="14sp" android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"

View File

@ -37,6 +37,10 @@ internal fun ChunkEntity.Companion.findAllIncludingEvents(realm: Realm, eventIds
.findAll() .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 { internal fun ChunkEntity.Companion.create(realm: Realm, prevToken: String?, nextToken: String?): ChunkEntity {
return realm.createObject<ChunkEntity>().apply { return realm.createObject<ChunkEntity>().apply {
this.prevToken = prevToken this.prevToken = prevToken

View File

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


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

fun extractFrom(event: EventEntity): RoomMember? { fun extractFrom(event: EventEntity): RoomMember? {
if (cached.containsKey(event.eventId)) {
return cached[event.eventId]
}
val sender = event.sender ?: return null val sender = event.sender ?: return null
// If the event is unlinked we want to fetch unlinked state events // If the event is unlinked we want to fetch unlinked state events
val unlinked = event.isUnlinked 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() // If prevContent is null we fallback to the Int.MIN state events content()
val content = if (event.stateIndex <= 0) { val content = if (event.stateIndex <= 0) {
baseQuery(monarchy, roomId, sender, unlinked).next(from = event.stateIndex)?.prevContent 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 { } else {
baseQuery(monarchy, roomId, sender, unlinked).last(since = event.stateIndex)?.content 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, 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 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( data class Params(
val roomId: String, val roomId: String,
@ -22,14 +22,13 @@ internal class DefaultPaginationTask(private val roomAPI: RoomAPI,
private val tokenChunkEventPersistor: TokenChunkEventPersistor private val tokenChunkEventPersistor: TokenChunkEventPersistor
) : PaginationTask { ) : PaginationTask {


override fun execute(params: PaginationTask.Params): Try<TokenChunkEvent> { override fun execute(params: PaginationTask.Params): Try<Boolean> {
val filter = FilterUtil.createRoomEventFilter(true)?.toJSONString() val filter = FilterUtil.createRoomEventFilter(true)?.toJSONString()
return executeRequest<PaginationResponse> { return executeRequest<PaginationResponse> {
apiCall = roomAPI.getRoomMessagesFrom(params.roomId, params.from, params.direction.value, params.limit, filter) apiCall = roomAPI.getRoomMessagesFrom(params.roomId, params.from, params.direction.value, params.limit, filter)
}.flatMap { chunk -> }.flatMap { chunk ->
tokenChunkEventPersistor tokenChunkEventPersistor
.insertInDb(chunk, params.roomId, params.direction) .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.Realm
import io.realm.RealmQuery 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, internal class DefaultTimelineService(private val roomId: String,
private val monarchy: Monarchy, private val monarchy: Monarchy,
@ -51,6 +52,7 @@ internal class DefaultTimelineService(private val roomId: String,
val pagedListConfig = PagedList.Config.Builder() val pagedListConfig = PagedList.Config.Builder()
.setEnablePlaceholders(false) .setEnablePlaceholders(false)
.setPageSize(PAGE_SIZE) .setPageSize(PAGE_SIZE)
.setPrefetchDistance(PREFETCH_DISTANCE)
.build() .build()


val livePagedListBuilder = LivePagedListBuilder(domainSourceFactory, pagedListConfig).setBoundaryCallback(boundaryCallback) 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.MatrixCallback
import im.vector.matrix.android.api.session.events.model.EnrichedEvent 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.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.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.PagingRequestHelper import im.vector.matrix.android.internal.util.PagingRequestHelper
import java.util.* import timber.log.Timber


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


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


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


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


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


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


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



View File

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


import arrow.core.Try import arrow.core.Try
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.internal.database.helper.addAll import im.vector.matrix.android.internal.database.helper.*
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.model.ChunkEntity 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.model.RoomEntity
import im.vector.matrix.android.internal.database.query.create import im.vector.matrix.android.internal.database.query.create
@ -21,12 +16,15 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {


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


if (receivedChunk.events.isEmpty() && receivedChunk.stateEvents.isEmpty()) {
return Try.just(false)
}
return monarchy return monarchy
.tryTransactionSync { realm -> .tryTransactionSync { realm ->
val roomEntity = RoomEntity.where(realm, roomId).findFirst() 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 nextToken: String?
val prevToken: String? val prevToken: String?
@ -46,10 +44,10 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {


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


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


private fun handleMerge(roomEntity: RoomEntity, private fun handleMerge(roomEntity: RoomEntity,