diff --git a/app/build.gradle b/app/build.gradle index 1caa4065..f96635de 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index fe1f3798..a03f21a5 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -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 } diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageItem.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageItem.kt index fb06ef9b..cdb5e35e 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageItem.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageItem.kt @@ -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?, diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TextItem.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TextItem.kt index 8848b58b..4b827a5a 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TextItem.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TextItem.kt @@ -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) { diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt index 88649561..0cd38e50 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt @@ -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( 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 = 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, - isLoadingForward: Boolean, - isLoadingBackward: Boolean) { - if (events.isEmpty()) { - return + override fun buildItemModels(currentPosition: Int, items: List): List> { + if (items.isNullOrEmpty()) { + return emptyList() } + val epoxyModels = ArrayList>() + 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>) { 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) diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/paging/PagedListEpoxyController.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/paging/PagedListEpoxyController.kt new file mode 100644 index 00000000..a01c2904 --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/paging/PagedListEpoxyController.kt @@ -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( + /** + * 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 = DEFAULT_ITEM_DIFF_CALLBACK as DiffUtil.ItemCallback +) : 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>) { + 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): List> + + /** + * 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?) { + 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() { + override fun areItemsTheSame(oldItem: Any, newItem: Any) = oldItem == newItem + + override fun areContentsTheSame(oldItem: Any, newItem: Any) = oldItem == newItem + } + } +} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/paging/PagedListModelCache.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/paging/PagedListModelCache.kt new file mode 100644 index 00000000..1966ecee --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/paging/PagedListModelCache.kt @@ -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( + private val modelBuilder: (itemIndex: Int, items: List) -> List>, + private val rebuildCallback: () -> Unit, + private val itemDiffCallback: DiffUtil.ItemCallback, + 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, 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( + updateCallback, + AsyncDifferConfig.Builder( + 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?) { + asyncDiffer.submitList(pagedList) + } + + fun getModels(): List> { + 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>) { + epoxyModels.forEach { + modelCache[it] = itemPosition + } + } + + private fun buildModel(pos: Int) { + if (pos >= asyncDiffer.currentList?.size ?: 0) { + return + } + modelBuilder(pos, asyncDiffer.currentList as List).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)) + } + } + } +} diff --git a/app/src/main/res/layout/item_event_message.xml b/app/src/main/res/layout/item_event_message.xml index 9ed6fe9b..c4b94cd4 100644 --- a/app/src/main/res/layout/item_event_message.xml +++ b/app/src/main/res/layout/item_event_message.xml @@ -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" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt index 2035af90..cf9cea43 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt @@ -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().apply { this.prevToken = prevToken diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMemberExtractor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMemberExtractor.kt index 88acac5e..e58f6f62 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMemberExtractor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMemberExtractor.kt @@ -15,7 +15,12 @@ import io.realm.RealmQuery internal class RoomMemberExtractor(private val monarchy: Monarchy, private val roomId: String) { + private val cached = HashMap() + 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, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultPaginationTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultPaginationTask.kt index a9a549a3..647515a6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultPaginationTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultPaginationTask.kt @@ -7,7 +7,7 @@ import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.FilterUtil -internal interface PaginationTask : Task { +internal interface PaginationTask : Task { 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 { + override fun execute(params: PaginationTask.Params): Try { val filter = FilterUtil.createRoomEventFilter(true)?.toJSONString() return executeRequest { apiCall = roomAPI.getRoomMessagesFrom(params.roomId, params.from, params.direction.value, params.limit, filter) }.flatMap { chunk -> tokenChunkEventPersistor .insertInDb(chunk, params.roomId, params.direction) - .map { chunk } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt index 401da51d..f28f48bf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt @@ -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) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineBoundaryCallback.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineBoundaryCallback.kt index a9ff32d0..db9617e0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineBoundaryCallback.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineBoundaryCallback.kt @@ -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 { - override fun onSuccess(data: TokenChunkEvent) { + .dispatchTo(object : MatrixCallback { + override fun onSuccess(data: Boolean) { requestCallback.recordSuccess() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt index 68f26bcc..7e5943ab 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt @@ -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 { + direction: PaginationDirection): Try { + 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,