forked from GitHub-Mirror/riotX-android
Timeline : try to get a better PagedList/Epoxy integration. Still need to be refined.
This commit is contained in:
parent
de90cbe73e
commit
922609cb57
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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?,
|
||||||
|
@ -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) {
|
||||||
|
|
||||||
|
@ -1,73 +1,46 @@
|
|||||||
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
|
|
||||||
}
|
}
|
||||||
LoadingItemModel_()
|
val epoxyModels = ArrayList<EpoxyModel<*>>()
|
||||||
.id(roomId + "forward_loading_item")
|
val event = items[currentPosition] ?: return emptyList()
|
||||||
.addIf(isLoadingForward, this)
|
val nextEvent = if (currentPosition + 1 < items.size) items[currentPosition + 1] else null
|
||||||
|
|
||||||
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 date = event.root.localDateTime()
|
||||||
val nextDate = nextEvent?.root?.localDateTime()
|
val nextDate = nextEvent?.root?.localDateTime()
|
||||||
@ -77,30 +50,31 @@ class TimelineEventController(private val roomId: String,
|
|||||||
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, addDaySeparator, date, callback)
|
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, addDaySeparator, date, callback)
|
||||||
else -> textItemFactory.create(event)
|
else -> textItemFactory.create(event)
|
||||||
}
|
}
|
||||||
|
item?.also {
|
||||||
item
|
it.id(event.localId)
|
||||||
?.onBind {
|
epoxyModels.add(it)
|
||||||
timelineData?.events?.loadAround(index)
|
|
||||||
}
|
}
|
||||||
?.id(event.localId)
|
|
||||||
?.addTo(this)
|
|
||||||
|
|
||||||
if (addDaySeparator) {
|
if (addDaySeparator) {
|
||||||
val formattedDay = dateFormatter.formatMessageDay(date)
|
val formattedDay = dateFormatter.formatMessageDay(date)
|
||||||
DaySeparatorItem(formattedDay).id(roomId + formattedDay).addTo(this)
|
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)
|
||||||
|
|
||||||
|
super.add(models)
|
||||||
|
|
||||||
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)
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
@ -27,7 +32,9 @@ internal class RoomMemberExtractor(private val monarchy: Monarchy,
|
|||||||
} 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,
|
||||||
|
@ -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 }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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,6 +43,7 @@ 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
|
||||||
|
|
||||||
@ -52,6 +53,7 @@ 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
|
||||||
|
|
||||||
@ -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
|
||||||
@ -80,8 +82,8 @@ internal class TimelineBoundaryCallback(private val roomId: String,
|
|||||||
|
|
||||||
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,8 +16,11 @@ 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()
|
||||||
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user