forked from GitHub-Mirror/riotX-android
Timeline : reactivate loaders and get off the main thread
This commit is contained in:
parent
0c76178bee
commit
2898eae566
@ -58,7 +58,7 @@ android {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
def epoxy_version = "3.0.0"
|
def epoxy_version = "3.3.0"
|
||||||
def arrow_version = "0.8.2"
|
def arrow_version = "0.8.2"
|
||||||
def coroutines_version = "1.0.1"
|
def coroutines_version = "1.0.1"
|
||||||
def markwon_version = '3.0.0-SNAPSHOT'
|
def markwon_version = '3.0.0-SNAPSHOT'
|
||||||
@ -77,9 +77,6 @@ dependencies {
|
|||||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||||
implementation 'androidx.core:core-ktx:1.0.1'
|
implementation 'androidx.core:core-ktx:1.0.1'
|
||||||
|
|
||||||
// Paging
|
|
||||||
implementation 'androidx.paging:paging-runtime:2.0.0'
|
|
||||||
|
|
||||||
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'
|
||||||
implementation 'com.facebook.stetho:stetho:1.5.0'
|
implementation 'com.facebook.stetho:stetho:1.5.0'
|
||||||
|
@ -19,6 +19,8 @@ package im.vector.riotredesign
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.multidex.MultiDex
|
import androidx.multidex.MultiDex
|
||||||
|
import com.airbnb.epoxy.EpoxyAsyncUtil
|
||||||
|
import com.airbnb.epoxy.EpoxyController
|
||||||
import com.facebook.stetho.Stetho
|
import com.facebook.stetho.Stetho
|
||||||
import com.github.piasy.biv.BigImageViewer
|
import com.github.piasy.biv.BigImageViewer
|
||||||
import com.github.piasy.biv.loader.glide.GlideImageLoader
|
import com.github.piasy.biv.loader.glide.GlideImageLoader
|
||||||
@ -41,6 +43,8 @@ class Riot : Application() {
|
|||||||
}
|
}
|
||||||
AndroidThreeTen.init(this)
|
AndroidThreeTen.init(this)
|
||||||
BigImageViewer.initialize(GlideImageLoader.with(applicationContext))
|
BigImageViewer.initialize(GlideImageLoader.with(applicationContext))
|
||||||
|
EpoxyController.defaultDiffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
|
||||||
|
EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
|
||||||
val appModule = AppModule(applicationContext).definition
|
val appModule = AppModule(applicationContext).definition
|
||||||
val homeModule = HomeModule().definition
|
val homeModule = HomeModule().definition
|
||||||
startKoin(listOf(appModule, homeModule), logger = EmptyLogger())
|
startKoin(listOf(appModule, homeModule), logger = EmptyLogger())
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package im.vector.riotredesign.features.home.room.detail
|
package im.vector.riotredesign.features.home.room.detail
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
|
|
||||||
sealed class RoomDetailActions {
|
sealed class RoomDetailActions {
|
||||||
@ -23,6 +24,6 @@ sealed class RoomDetailActions {
|
|||||||
data class SendMessage(val text: String) : RoomDetailActions()
|
data class SendMessage(val text: String) : RoomDetailActions()
|
||||||
object IsDisplayed : RoomDetailActions()
|
object IsDisplayed : RoomDetailActions()
|
||||||
data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions()
|
data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions()
|
||||||
object LoadMore: RoomDetailActions()
|
data class LoadMore(val direction: Timeline.Direction) : RoomDetailActions()
|
||||||
|
|
||||||
}
|
}
|
@ -25,6 +25,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.airbnb.epoxy.EpoxyVisibilityTracker
|
import com.airbnb.epoxy.EpoxyVisibilityTracker
|
||||||
import com.airbnb.mvrx.fragmentViewModel
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
|
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.riotredesign.R
|
import im.vector.riotredesign.R
|
||||||
import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
|
import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
|
||||||
@ -110,9 +111,14 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
|
|||||||
it.dispatchTo(stateRestorer)
|
it.dispatchTo(stateRestorer)
|
||||||
it.dispatchTo(scrollOnNewMessageCallback)
|
it.dispatchTo(scrollOnNewMessageCallback)
|
||||||
}
|
}
|
||||||
recyclerView.addOnScrollListener(object : EndlessRecyclerViewScrollListener(layoutManager, EndlessRecyclerViewScrollListener.LoadOnScrollDirection.BOTTOM) {
|
recyclerView.addOnScrollListener(object : EndlessRecyclerViewScrollListener(layoutManager, Timeline.Direction.BACKWARDS) {
|
||||||
override fun onLoadMore(page: Int, totalItemsCount: Int) {
|
override fun onLoadMore() {
|
||||||
roomDetailViewModel.process(RoomDetailActions.LoadMore)
|
roomDetailViewModel.process(RoomDetailActions.LoadMore(Timeline.Direction.BACKWARDS))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
recyclerView.addOnScrollListener(object : EndlessRecyclerViewScrollListener(layoutManager, Timeline.Direction.FORWARDS) {
|
||||||
|
override fun onLoadMore() {
|
||||||
|
roomDetailViewModel.process(RoomDetailActions.LoadMore(Timeline.Direction.FORWARDS))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
recyclerView.setController(timelineEventController)
|
recyclerView.setController(timelineEventController)
|
||||||
|
@ -22,12 +22,12 @@ import com.jakewharton.rxrelay2.BehaviorRelay
|
|||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
|
||||||
import im.vector.matrix.rx.rx
|
import im.vector.matrix.rx.rx
|
||||||
import im.vector.riotredesign.core.platform.RiotViewModel
|
import im.vector.riotredesign.core.platform.RiotViewModel
|
||||||
import im.vector.riotredesign.features.home.room.VisibleRoomStore
|
import im.vector.riotredesign.features.home.room.VisibleRoomStore
|
||||||
import io.reactivex.rxkotlin.subscribeBy
|
import io.reactivex.rxkotlin.subscribeBy
|
||||||
import org.koin.android.ext.android.get
|
import org.koin.android.ext.android.get
|
||||||
|
import timber.log.Timber
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class RoomDetailViewModel(initialState: RoomDetailViewState,
|
class RoomDetailViewModel(initialState: RoomDetailViewState,
|
||||||
@ -64,7 +64,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
|||||||
is RoomDetailActions.SendMessage -> handleSendMessage(action)
|
is RoomDetailActions.SendMessage -> handleSendMessage(action)
|
||||||
is RoomDetailActions.IsDisplayed -> handleIsDisplayed()
|
is RoomDetailActions.IsDisplayed -> handleIsDisplayed()
|
||||||
is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action)
|
is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action)
|
||||||
is RoomDetailActions.LoadMore -> timeline.paginate(Timeline.Direction.BACKWARDS, 50)
|
is RoomDetailActions.LoadMore -> handleLoadMore(action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,6 +82,10 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
|||||||
visibleRoomHolder.post(roomId)
|
visibleRoomHolder.post(roomId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleLoadMore(action: RoomDetailActions.LoadMore) {
|
||||||
|
timeline.paginate(action.direction, 50)
|
||||||
|
}
|
||||||
|
|
||||||
private fun observeDisplayedEvents() {
|
private fun observeDisplayedEvents() {
|
||||||
// We are buffering scroll events for one second
|
// We are buffering scroll events for one second
|
||||||
// and keep the most recent one to set the read receipt on.
|
// and keep the most recent one to set the read receipt on.
|
||||||
@ -100,13 +104,14 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
|||||||
private fun observeRoomSummary() {
|
private fun observeRoomSummary() {
|
||||||
room.rx().liveRoomSummary()
|
room.rx().liveRoomSummary()
|
||||||
.execute { async ->
|
.execute { async ->
|
||||||
|
Timber.v("Room summary updated: $async")
|
||||||
copy(asyncRoomSummary = async)
|
copy(asyncRoomSummary = async)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
|
||||||
timeline.dispose()
|
timeline.dispose()
|
||||||
|
super.onCleared()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -16,19 +16,21 @@
|
|||||||
|
|
||||||
package im.vector.riotredesign.features.home.room.detail.timeline
|
package im.vector.riotredesign.features.home.room.detail.timeline
|
||||||
|
|
||||||
|
import android.os.Handler
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.ListUpdateCallback
|
import androidx.recyclerview.widget.ListUpdateCallback
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.airbnb.epoxy.EpoxyAsyncUtil
|
|
||||||
import com.airbnb.epoxy.EpoxyController
|
import com.airbnb.epoxy.EpoxyController
|
||||||
import com.airbnb.epoxy.EpoxyModel
|
import com.airbnb.epoxy.EpoxyModel
|
||||||
import com.airbnb.epoxy.VisibilityState
|
import com.airbnb.epoxy.VisibilityState
|
||||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
|
import im.vector.riotredesign.core.epoxy.LoadingItemModel_
|
||||||
import im.vector.riotredesign.core.epoxy.RiotEpoxyModel
|
import im.vector.riotredesign.core.epoxy.RiotEpoxyModel
|
||||||
import im.vector.riotredesign.core.extensions.localDateTime
|
import im.vector.riotredesign.core.extensions.localDateTime
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.factory.TimelineItemFactory
|
import im.vector.riotredesign.features.home.room.detail.timeline.factory.TimelineItemFactory
|
||||||
|
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineAsyncHelper
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter
|
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback
|
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
||||||
@ -37,20 +39,16 @@ import im.vector.riotredesign.features.media.MediaContentRenderer
|
|||||||
|
|
||||||
class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
||||||
private val timelineItemFactory: TimelineItemFactory,
|
private val timelineItemFactory: TimelineItemFactory,
|
||||||
private val timelineMediaSizeProvider: TimelineMediaSizeProvider
|
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
|
||||||
) : EpoxyController(
|
private val backgroundHandler: Handler = TimelineAsyncHelper.getBackgroundHandler()
|
||||||
EpoxyAsyncUtil.getAsyncBackgroundHandler(),
|
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener {
|
||||||
EpoxyAsyncUtil.getAsyncBackgroundHandler()
|
|
||||||
), Timeline.Listener {
|
|
||||||
|
|
||||||
private val modelCache = arrayListOf<List<EpoxyModel<*>>>()
|
private val modelCache = arrayListOf<List<EpoxyModel<*>>>()
|
||||||
private var currentSnapshot: List<TimelineEvent> = emptyList()
|
private var currentSnapshot: List<TimelineEvent> = emptyList()
|
||||||
|
|
||||||
override fun onUpdated(snapshot: List<TimelineEvent>) {
|
|
||||||
submitSnapshot(snapshot)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val listUpdateCallback = object : ListUpdateCallback {
|
private val listUpdateCallback = object : ListUpdateCallback {
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
||||||
(position until (position + count)).forEach {
|
(position until (position + count)).forEach {
|
||||||
modelCache[it] = emptyList()
|
modelCache[it] = emptyList()
|
||||||
@ -62,7 +60,8 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||||||
//no-op
|
//no-op
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onInserted(position: Int, count: Int) {
|
@Synchronized
|
||||||
|
override fun onInserted(position: Int, count: Int) = synchronized(modelCache) {
|
||||||
if (modelCache.isNotEmpty() && position == modelCache.size) {
|
if (modelCache.isNotEmpty() && position == modelCache.size) {
|
||||||
modelCache[position - 1] = emptyList()
|
modelCache[position - 1] = emptyList()
|
||||||
}
|
}
|
||||||
@ -78,10 +77,6 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isLoadingForward: Boolean = false
|
|
||||||
private var isLoadingBackward: Boolean = false
|
|
||||||
private var hasReachedEnd: Boolean = true
|
|
||||||
|
|
||||||
private var timeline: Timeline? = null
|
private var timeline: Timeline? = null
|
||||||
var callback: Callback? = null
|
var callback: Callback? = null
|
||||||
|
|
||||||
@ -89,7 +84,6 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||||||
if (this.timeline != timeline) {
|
if (this.timeline != timeline) {
|
||||||
this.timeline = timeline
|
this.timeline = timeline
|
||||||
this.timeline?.listener = this
|
this.timeline?.listener = this
|
||||||
submitSnapshot(timeline?.snapshot() ?: emptyList())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,9 +93,25 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun buildModels() {
|
override fun buildModels() {
|
||||||
|
LoadingItemModel_()
|
||||||
|
.id("forward_loading_item")
|
||||||
|
.addWhen(Timeline.Direction.FORWARDS)
|
||||||
|
|
||||||
add(getModels())
|
add(getModels())
|
||||||
|
|
||||||
|
LoadingItemModel_()
|
||||||
|
.id("backward_loading_item")
|
||||||
|
.addWhen(Timeline.Direction.BACKWARDS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun LoadingItemModel_.addWhen(direction: Timeline.Direction) {
|
||||||
|
val shouldAdd = timeline?.let {
|
||||||
|
it.hasMoreToLoad(direction) || !it.hasReachedEnd(direction)
|
||||||
|
} ?: false
|
||||||
|
addIf(shouldAdd, this@TimelineEventController)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
private fun getModels(): List<EpoxyModel<*>> {
|
private fun getModels(): List<EpoxyModel<*>> {
|
||||||
(0 until modelCache.size).forEach { position ->
|
(0 until modelCache.size).forEach { position ->
|
||||||
if (modelCache[position].isEmpty()) {
|
if (modelCache[position].isEmpty()) {
|
||||||
@ -133,8 +143,14 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||||||
return epoxyModels
|
return epoxyModels
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Timeline.LISTENER ***************************************************************************
|
||||||
|
|
||||||
|
override fun onUpdated(snapshot: List<TimelineEvent>) {
|
||||||
|
submitSnapshot(snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
private fun submitSnapshot(newSnapshot: List<TimelineEvent>) {
|
private fun submitSnapshot(newSnapshot: List<TimelineEvent>) {
|
||||||
EpoxyAsyncUtil.getAsyncBackgroundHandler().post {
|
backgroundHandler.post {
|
||||||
val diffCallback = TimelineEventDiffUtilCallback(currentSnapshot, newSnapshot)
|
val diffCallback = TimelineEventDiffUtilCallback(currentSnapshot, newSnapshot)
|
||||||
currentSnapshot = newSnapshot
|
currentSnapshot = newSnapshot
|
||||||
val diffResult = DiffUtil.calculateDiff(diffCallback)
|
val diffResult = DiffUtil.calculateDiff(diffCallback)
|
||||||
@ -142,6 +158,7 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
fun onEventVisible(event: TimelineEvent)
|
fun onEventVisible(event: TimelineEvent)
|
||||||
fun onUrlClicked(url: String)
|
fun onUrlClicked(url: String)
|
||||||
|
@ -19,26 +19,24 @@ package im.vector.riotredesign.features.home.room.detail.timeline.helper;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import im.vector.matrix.android.api.session.room.timeline.Timeline;
|
||||||
|
|
||||||
// Todo rework that, it has been copy/paste at the moment
|
|
||||||
public abstract class EndlessRecyclerViewScrollListener extends RecyclerView.OnScrollListener {
|
public abstract class EndlessRecyclerViewScrollListener extends RecyclerView.OnScrollListener {
|
||||||
// Sets the starting page index
|
// Sets the starting page index
|
||||||
private static final int startingPageIndex = 0;
|
private static final int startingPageIndex = 0;
|
||||||
// The minimum amount of items to have below your current scroll position
|
// The minimum amount of items to have below your current scroll position
|
||||||
// before loading more.
|
// before loading more.
|
||||||
private int visibleThreshold = 30;
|
private int visibleThreshold = 50;
|
||||||
// The current offset index of data you have loaded
|
|
||||||
private int currentPage = 0;
|
|
||||||
// The total number of items in the dataset after the last load
|
// The total number of items in the dataset after the last load
|
||||||
private int previousTotalItemCount = 0;
|
private int previousTotalItemCount = 0;
|
||||||
// True if we are still waiting for the last set of data to load.
|
// True if we are still waiting for the last set of data to load.
|
||||||
private boolean loading = true;
|
private boolean loading = true;
|
||||||
private LinearLayoutManager mLayoutManager;
|
private LinearLayoutManager mLayoutManager;
|
||||||
private LoadOnScrollDirection mDirection;
|
private Timeline.Direction mDirection;
|
||||||
|
|
||||||
public EndlessRecyclerViewScrollListener(LinearLayoutManager layoutManager, LoadOnScrollDirection direction) {
|
public EndlessRecyclerViewScrollListener(LinearLayoutManager layoutManager, Timeline.Direction direction) {
|
||||||
this.mLayoutManager = layoutManager;
|
this.mLayoutManager = layoutManager;
|
||||||
mDirection = direction;
|
this.mDirection = direction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -55,11 +53,10 @@ public abstract class EndlessRecyclerViewScrollListener extends RecyclerView.OnS
|
|||||||
firstVisibleItemPosition = mLayoutManager.findFirstVisibleItemPosition();
|
firstVisibleItemPosition = mLayoutManager.findFirstVisibleItemPosition();
|
||||||
|
|
||||||
switch (mDirection) {
|
switch (mDirection) {
|
||||||
case BOTTOM:
|
case BACKWARDS:
|
||||||
// If the total item count is zero and the previous isn't, assume the
|
// If the total item count is zero and the previous isn't, assume the
|
||||||
// list is invalidated and should be reset back to initial state
|
// list is invalidated and should be reset back to initial state
|
||||||
if (totalItemCount < previousTotalItemCount) {
|
if (totalItemCount < previousTotalItemCount) {
|
||||||
this.currentPage = startingPageIndex;
|
|
||||||
this.previousTotalItemCount = totalItemCount;
|
this.previousTotalItemCount = totalItemCount;
|
||||||
if (totalItemCount == 0) {
|
if (totalItemCount == 0) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
@ -78,16 +75,14 @@ public abstract class EndlessRecyclerViewScrollListener extends RecyclerView.OnS
|
|||||||
// If we do need to reload some more data, we execute onLoadMore to fetch the data.
|
// If we do need to reload some more data, we execute onLoadMore to fetch the data.
|
||||||
// threshold should reflect how many total columns there are too
|
// threshold should reflect how many total columns there are too
|
||||||
if (!loading && (lastVisibleItemPosition + visibleThreshold) > totalItemCount) {
|
if (!loading && (lastVisibleItemPosition + visibleThreshold) > totalItemCount) {
|
||||||
currentPage++;
|
onLoadMore();
|
||||||
onLoadMore(currentPage, totalItemCount);
|
|
||||||
loading = true;
|
loading = true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case TOP:
|
case FORWARDS:
|
||||||
// If the total item count is zero and the previous isn't, assume the
|
// If the total item count is zero and the previous isn't, assume the
|
||||||
// list is invalidated and should be reset back to initial state
|
// list is invalidated and should be reset back to initial state
|
||||||
if (totalItemCount < previousTotalItemCount) {
|
if (totalItemCount < previousTotalItemCount) {
|
||||||
this.currentPage = startingPageIndex;
|
|
||||||
this.previousTotalItemCount = totalItemCount;
|
this.previousTotalItemCount = totalItemCount;
|
||||||
if (totalItemCount == 0) {
|
if (totalItemCount == 0) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
@ -106,42 +101,14 @@ public abstract class EndlessRecyclerViewScrollListener extends RecyclerView.OnS
|
|||||||
// If we do need to reload some more data, we execute onLoadMore to fetch the data.
|
// If we do need to reload some more data, we execute onLoadMore to fetch the data.
|
||||||
// threshold should reflect how many total columns there are too
|
// threshold should reflect how many total columns there are too
|
||||||
if (!loading && firstVisibleItemPosition < visibleThreshold) {
|
if (!loading && firstVisibleItemPosition < visibleThreshold) {
|
||||||
currentPage++;
|
onLoadMore();
|
||||||
onLoadMore(currentPage, totalItemCount);
|
|
||||||
loading = true;
|
loading = true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getLastVisibleItem(int[] lastVisibleItemPositions) {
|
|
||||||
int maxSize = 0;
|
|
||||||
for (int i = 0; i < lastVisibleItemPositions.length; i++) {
|
|
||||||
if (i == 0) {
|
|
||||||
maxSize = lastVisibleItemPositions[i];
|
|
||||||
} else if (lastVisibleItemPositions[i] > maxSize) {
|
|
||||||
maxSize = lastVisibleItemPositions[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return maxSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getFirstVisibleItem(int[] firstVisibleItemPositions) {
|
|
||||||
int maxSize = 0;
|
|
||||||
for (int i = 0; i < firstVisibleItemPositions.length; i++) {
|
|
||||||
if (i == 0) {
|
|
||||||
maxSize = firstVisibleItemPositions[i];
|
|
||||||
} else if (firstVisibleItemPositions[i] > maxSize) {
|
|
||||||
maxSize = firstVisibleItemPositions[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return maxSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Defines the process for actually loading more data based on page
|
// Defines the process for actually loading more data based on page
|
||||||
public abstract void onLoadMore(int page, int totalItemsCount);
|
public abstract void onLoadMore();
|
||||||
|
|
||||||
public enum LoadOnScrollDirection {
|
|
||||||
TOP, BOTTOM
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
*
|
||||||
|
* * Copyright 2019 New Vector Ltd
|
||||||
|
* *
|
||||||
|
* * 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.helper
|
||||||
|
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.HandlerThread
|
||||||
|
|
||||||
|
private const val THREAD_NAME = "Timeline_Building_Thread"
|
||||||
|
|
||||||
|
object TimelineAsyncHelper {
|
||||||
|
|
||||||
|
private var backgroundHandlerThread: HandlerThread? = null
|
||||||
|
private var backgroundHandler: Handler? = null
|
||||||
|
|
||||||
|
fun getBackgroundHandler(): Handler {
|
||||||
|
if (backgroundHandler != null) {
|
||||||
|
backgroundHandler?.removeCallbacksAndMessages(null)
|
||||||
|
}
|
||||||
|
if (backgroundHandlerThread != null) {
|
||||||
|
backgroundHandlerThread?.quit()
|
||||||
|
}
|
||||||
|
val handlerThread = HandlerThread(THREAD_NAME)
|
||||||
|
.also {
|
||||||
|
backgroundHandlerThread = it
|
||||||
|
it.start()
|
||||||
|
}
|
||||||
|
val looper = handlerThread.looper
|
||||||
|
return Handler(looper).also { backgroundHandler = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -76,9 +76,6 @@ dependencies {
|
|||||||
implementation 'com.github.Zhuinden:realm-monarchy:0.5.1'
|
implementation 'com.github.Zhuinden:realm-monarchy:0.5.1'
|
||||||
kapt 'dk.ilios:realmfieldnameshelper:1.1.1'
|
kapt 'dk.ilios:realmfieldnameshelper:1.1.1'
|
||||||
|
|
||||||
// Paging
|
|
||||||
implementation 'androidx.paging:paging-runtime:2.0.0'
|
|
||||||
|
|
||||||
// Work
|
// Work
|
||||||
implementation "android.arch.work:work-runtime-ktx:1.0.0-beta02"
|
implementation "android.arch.work:work-runtime-ktx:1.0.0-beta02"
|
||||||
|
|
||||||
|
@ -134,13 +134,13 @@ internal class ChunkEntityTest : InstrumentedTest {
|
|||||||
val chunk2: ChunkEntity = realm.createObject()
|
val chunk2: ChunkEntity = realm.createObject()
|
||||||
val eventsForChunk1 = createFakeListOfEvents(30)
|
val eventsForChunk1 = createFakeListOfEvents(30)
|
||||||
val eventsForChunk2 = eventsForChunk1 + createFakeListOfEvents(10)
|
val eventsForChunk2 = eventsForChunk1 + createFakeListOfEvents(10)
|
||||||
chunk1.isLast = true
|
chunk1.isLastForward = true
|
||||||
chunk2.isLast = false
|
chunk2.isLastForward = false
|
||||||
chunk1.addAll("roomId", eventsForChunk1, PaginationDirection.FORWARDS)
|
chunk1.addAll("roomId", eventsForChunk1, PaginationDirection.FORWARDS)
|
||||||
chunk2.addAll("roomId", eventsForChunk2, PaginationDirection.BACKWARDS)
|
chunk2.addAll("roomId", eventsForChunk2, PaginationDirection.BACKWARDS)
|
||||||
chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS)
|
chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS)
|
||||||
chunk1.events.size shouldEqual 40
|
chunk1.events.size shouldEqual 40
|
||||||
chunk1.isLast.shouldBeTrue()
|
chunk1.isLastForward.shouldBeTrue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ import kotlin.random.Random
|
|||||||
|
|
||||||
internal class FakeGetContextOfEventTask(private val tokenChunkEventPersistor: TokenChunkEventPersistor) : GetContextOfEventTask {
|
internal class FakeGetContextOfEventTask(private val tokenChunkEventPersistor: TokenChunkEventPersistor) : GetContextOfEventTask {
|
||||||
|
|
||||||
override fun execute(params: GetContextOfEventTask.Params): Try<TokenChunkEvent> {
|
override fun execute(params: GetContextOfEventTask.Params): Try<TokenChunkEventPersistor.Result> {
|
||||||
val fakeEvents = RoomDataHelper.createFakeListOfEvents(30)
|
val fakeEvents = RoomDataHelper.createFakeListOfEvents(30)
|
||||||
val tokenChunkEvent = FakeTokenChunkEvent(
|
val tokenChunkEvent = FakeTokenChunkEvent(
|
||||||
Random.nextLong(System.currentTimeMillis()).toString(),
|
Random.nextLong(System.currentTimeMillis()).toString(),
|
||||||
@ -33,7 +33,6 @@ internal class FakeGetContextOfEventTask(private val tokenChunkEventPersistor: T
|
|||||||
fakeEvents
|
fakeEvents
|
||||||
)
|
)
|
||||||
return tokenChunkEventPersistor.insertInDb(tokenChunkEvent, params.roomId, PaginationDirection.BACKWARDS)
|
return tokenChunkEventPersistor.insertInDb(tokenChunkEvent, params.roomId, PaginationDirection.BACKWARDS)
|
||||||
.map { tokenChunkEvent }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -18,17 +18,15 @@ package im.vector.matrix.android.session.room.timeline
|
|||||||
|
|
||||||
import arrow.core.Try
|
import arrow.core.Try
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationTask
|
import im.vector.matrix.android.internal.session.room.timeline.PaginationTask
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEvent
|
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor
|
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
internal class FakePaginationTask(private val tokenChunkEventPersistor: TokenChunkEventPersistor) : PaginationTask {
|
internal class FakePaginationTask(private val tokenChunkEventPersistor: TokenChunkEventPersistor) : PaginationTask {
|
||||||
|
|
||||||
override fun execute(params: PaginationTask.Params): Try<TokenChunkEvent> {
|
override fun execute(params: PaginationTask.Params): Try<TokenChunkEventPersistor.Result> {
|
||||||
val fakeEvents = RoomDataHelper.createFakeListOfEvents(30)
|
val fakeEvents = RoomDataHelper.createFakeListOfEvents(30)
|
||||||
val tokenChunkEvent = FakeTokenChunkEvent(params.from, Random.nextLong(System.currentTimeMillis()).toString(), fakeEvents)
|
val tokenChunkEvent = FakeTokenChunkEvent(params.from, Random.nextLong(System.currentTimeMillis()).toString(), fakeEvents)
|
||||||
return tokenChunkEventPersistor.insertInDb(tokenChunkEvent, params.roomId, params.direction)
|
return tokenChunkEventPersistor.insertInDb(tokenChunkEvent, params.roomId, params.direction)
|
||||||
.map { tokenChunkEvent }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -48,7 +48,7 @@ object RoomDataHelper {
|
|||||||
val chunkEntity = realm.createObject<ChunkEntity>().apply {
|
val chunkEntity = realm.createObject<ChunkEntity>().apply {
|
||||||
nextToken = null
|
nextToken = null
|
||||||
prevToken = Random.nextLong(System.currentTimeMillis()).toString()
|
prevToken = Random.nextLong(System.currentTimeMillis()).toString()
|
||||||
isLast = true
|
isLastForward = true
|
||||||
}
|
}
|
||||||
chunkEntity.addAll("roomId", eventList, PaginationDirection.FORWARDS)
|
chunkEntity.addAll("roomId", eventList, PaginationDirection.FORWARDS)
|
||||||
roomEntity.addOrUpdate(chunkEntity)
|
roomEntity.addOrUpdate(chunkEntity)
|
||||||
|
@ -22,6 +22,8 @@ interface Timeline {
|
|||||||
|
|
||||||
var listener: Timeline.Listener?
|
var listener: Timeline.Listener?
|
||||||
|
|
||||||
|
fun hasMoreToLoad(direction: Direction): Boolean
|
||||||
|
fun hasReachedEnd(direction: Direction): Boolean
|
||||||
fun size(): Int
|
fun size(): Int
|
||||||
fun snapshot(): List<TimelineEvent>
|
fun snapshot(): List<TimelineEvent>
|
||||||
fun paginate(direction: Direction, count: Int)
|
fun paginate(direction: Direction, count: Int)
|
||||||
@ -44,14 +46,6 @@ interface Timeline {
|
|||||||
* These events come from a back pagination.
|
* These events come from a back pagination.
|
||||||
*/
|
*/
|
||||||
BACKWARDS("b");
|
BACKWARDS("b");
|
||||||
|
|
||||||
fun reversed(): Direction {
|
|
||||||
return when (this) {
|
|
||||||
FORWARDS -> BACKWARDS
|
|
||||||
BACKWARDS -> FORWARDS
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* 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.matrix.android.api.util
|
||||||
|
|
||||||
|
class CancelableBag : Cancelable {
|
||||||
|
|
||||||
|
private val cancelableList = ArrayList<Cancelable>()
|
||||||
|
|
||||||
|
fun add(cancelable: Cancelable) {
|
||||||
|
cancelableList.add(cancelable)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun cancel() {
|
||||||
|
cancelableList.forEach { it.cancel() }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Cancelable.addTo(cancelables: CancelableBag) {
|
||||||
|
cancelables.add(this)
|
||||||
|
}
|
@ -43,7 +43,6 @@ internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val m
|
|||||||
queryResults.addChangeListener { t, changeSet ->
|
queryResults.addChangeListener { t, changeSet ->
|
||||||
onChanged(t, changeSet)
|
onChanged(t, changeSet)
|
||||||
}
|
}
|
||||||
processInitialResults(queryResults)
|
|
||||||
results = AtomicReference(queryResults)
|
results = AtomicReference(queryResults)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -61,7 +60,7 @@ internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val m
|
|||||||
return isStarted.get()
|
return isStarted.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun onChanged(realmResults: RealmResults<T>, changeSet: OrderedCollectionChangeSet) {
|
private fun onChanged(realmResults: RealmResults<T>, changeSet: OrderedCollectionChangeSet) {
|
||||||
val insertionIndexes = changeSet.insertions
|
val insertionIndexes = changeSet.insertions
|
||||||
val updateIndexes = changeSet.changes
|
val updateIndexes = changeSet.changes
|
||||||
val deletionIndexes = changeSet.deletions
|
val deletionIndexes = changeSet.deletions
|
||||||
@ -71,12 +70,6 @@ internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val m
|
|||||||
processChanges(inserted, updated, deleted)
|
processChanges(inserted, updated, deleted)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun processInitialResults(results: RealmResults<T>) {
|
protected abstract fun processChanges(inserted: List<T>, updated: List<T>, deleted: List<T>)
|
||||||
// no-op
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun processChanges(inserted: List<T>, updated: List<T>, deleted: List<T>) {
|
|
||||||
//no-op
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
@ -27,18 +27,18 @@ import im.vector.matrix.android.internal.database.query.fastContains
|
|||||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
|
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
|
||||||
import io.realm.Sort
|
import io.realm.Sort
|
||||||
|
|
||||||
internal fun ChunkEntity.deleteOnCascade() {
|
|
||||||
assertIsManaged()
|
|
||||||
this.events.deleteAllFromRealm()
|
|
||||||
this.deleteFromRealm()
|
|
||||||
}
|
|
||||||
|
|
||||||
// By default if a chunk is empty we consider it unlinked
|
// By default if a chunk is empty we consider it unlinked
|
||||||
internal fun ChunkEntity.isUnlinked(): Boolean {
|
internal fun ChunkEntity.isUnlinked(): Boolean {
|
||||||
assertIsManaged()
|
assertIsManaged()
|
||||||
return events.where().equalTo(EventEntityFields.IS_UNLINKED, false).findAll().isEmpty()
|
return events.where().equalTo(EventEntityFields.IS_UNLINKED, false).findAll().isEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun ChunkEntity.deleteOnCascade() {
|
||||||
|
assertIsManaged()
|
||||||
|
this.events.deleteAllFromRealm()
|
||||||
|
this.deleteFromRealm()
|
||||||
|
}
|
||||||
|
|
||||||
internal fun ChunkEntity.merge(roomId: String,
|
internal fun ChunkEntity.merge(roomId: String,
|
||||||
chunkToMerge: ChunkEntity,
|
chunkToMerge: ChunkEntity,
|
||||||
direction: PaginationDirection) {
|
direction: PaginationDirection) {
|
||||||
@ -53,10 +53,11 @@ internal fun ChunkEntity.merge(roomId: String,
|
|||||||
val eventsToMerge: List<EventEntity>
|
val eventsToMerge: List<EventEntity>
|
||||||
if (direction == PaginationDirection.FORWARDS) {
|
if (direction == PaginationDirection.FORWARDS) {
|
||||||
this.nextToken = chunkToMerge.nextToken
|
this.nextToken = chunkToMerge.nextToken
|
||||||
this.isLast = chunkToMerge.isLast
|
this.isLastForward = chunkToMerge.isLastForward
|
||||||
eventsToMerge = chunkToMerge.events.reversed()
|
eventsToMerge = chunkToMerge.events.reversed()
|
||||||
} else {
|
} else {
|
||||||
this.prevToken = chunkToMerge.prevToken
|
this.prevToken = chunkToMerge.prevToken
|
||||||
|
this.isLastBackward = chunkToMerge.isLastBackward
|
||||||
eventsToMerge = chunkToMerge.events
|
eventsToMerge = chunkToMerge.events
|
||||||
}
|
}
|
||||||
eventsToMerge.forEach {
|
eventsToMerge.forEach {
|
||||||
|
@ -23,8 +23,9 @@ import io.realm.annotations.LinkingObjects
|
|||||||
|
|
||||||
internal open class ChunkEntity(var prevToken: String? = null,
|
internal open class ChunkEntity(var prevToken: String? = null,
|
||||||
var nextToken: String? = null,
|
var nextToken: String? = null,
|
||||||
var isLast: Boolean = false,
|
var events: RealmList<EventEntity> = RealmList(),
|
||||||
var events: RealmList<EventEntity> = RealmList()
|
var isLastForward: Boolean = false,
|
||||||
|
var isLastBackward: Boolean = false
|
||||||
) : RealmObject() {
|
) : RealmObject() {
|
||||||
|
|
||||||
@LinkingObjects("chunks")
|
@LinkingObjects("chunks")
|
||||||
|
@ -43,7 +43,7 @@ internal fun ChunkEntity.Companion.find(realm: Realm, roomId: String, prevToken:
|
|||||||
|
|
||||||
internal fun ChunkEntity.Companion.findLastLiveChunkFromRoom(realm: Realm, roomId: String): ChunkEntity? {
|
internal fun ChunkEntity.Companion.findLastLiveChunkFromRoom(realm: Realm, roomId: String): ChunkEntity? {
|
||||||
return where(realm, roomId)
|
return where(realm, roomId)
|
||||||
.equalTo(ChunkEntityFields.IS_LAST, true)
|
.equalTo(ChunkEntityFields.IS_LAST_FORWARD, true)
|
||||||
.findFirst()
|
.findFirst()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,12 +17,12 @@
|
|||||||
package im.vector.matrix.android.internal.session.room.timeline
|
package im.vector.matrix.android.internal.session.room.timeline
|
||||||
|
|
||||||
import arrow.core.Try
|
import arrow.core.Try
|
||||||
import im.vector.matrix.android.internal.task.Task
|
|
||||||
import im.vector.matrix.android.internal.network.executeRequest
|
import im.vector.matrix.android.internal.network.executeRequest
|
||||||
import im.vector.matrix.android.internal.session.room.RoomAPI
|
import im.vector.matrix.android.internal.session.room.RoomAPI
|
||||||
|
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 GetContextOfEventTask : Task<GetContextOfEventTask.Params, TokenChunkEvent> {
|
internal interface GetContextOfEventTask : Task<GetContextOfEventTask.Params, TokenChunkEventPersistor.Result> {
|
||||||
|
|
||||||
data class Params(
|
data class Params(
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
@ -35,12 +35,12 @@ internal class DefaultGetContextOfEventTask(private val roomAPI: RoomAPI,
|
|||||||
private val tokenChunkEventPersistor: TokenChunkEventPersistor
|
private val tokenChunkEventPersistor: TokenChunkEventPersistor
|
||||||
) : GetContextOfEventTask {
|
) : GetContextOfEventTask {
|
||||||
|
|
||||||
override fun execute(params: GetContextOfEventTask.Params): Try<EventContextResponse> {
|
override fun execute(params: GetContextOfEventTask.Params): Try<TokenChunkEventPersistor.Result> {
|
||||||
val filter = FilterUtil.createRoomEventFilter(true)?.toJSONString()
|
val filter = FilterUtil.createRoomEventFilter(true)?.toJSONString()
|
||||||
return executeRequest<EventContextResponse> {
|
return executeRequest<EventContextResponse> {
|
||||||
apiCall = roomAPI.getContextOfEvent(params.roomId, params.eventId, 0, filter)
|
apiCall = roomAPI.getContextOfEvent(params.roomId, params.eventId, 0, filter)
|
||||||
}.flatMap { response ->
|
}.flatMap { response ->
|
||||||
tokenChunkEventPersistor.insertInDb(response, params.roomId, PaginationDirection.BACKWARDS).map { response }
|
tokenChunkEventPersistor.insertInDb(response, params.roomId, PaginationDirection.BACKWARDS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,7 +23,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, Boolean> {
|
internal interface PaginationTask : Task<PaginationTask.Params, TokenChunkEventPersistor.Result> {
|
||||||
|
|
||||||
data class Params(
|
data class Params(
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
@ -38,7 +38,7 @@ internal class DefaultPaginationTask(private val roomAPI: RoomAPI,
|
|||||||
private val tokenChunkEventPersistor: TokenChunkEventPersistor
|
private val tokenChunkEventPersistor: TokenChunkEventPersistor
|
||||||
) : PaginationTask {
|
) : PaginationTask {
|
||||||
|
|
||||||
override fun execute(params: PaginationTask.Params): Try<Boolean> {
|
override fun execute(params: PaginationTask.Params): Try<TokenChunkEventPersistor.Result> {
|
||||||
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)
|
||||||
|
@ -18,10 +18,15 @@
|
|||||||
|
|
||||||
package im.vector.matrix.android.internal.session.room.timeline
|
package im.vector.matrix.android.internal.session.room.timeline
|
||||||
|
|
||||||
import androidx.annotation.UiThread
|
import android.os.Handler
|
||||||
|
import android.os.HandlerThread
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
|
import im.vector.matrix.android.api.util.CancelableBag
|
||||||
|
import im.vector.matrix.android.api.util.addTo
|
||||||
|
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||||
import im.vector.matrix.android.internal.database.model.ChunkEntityFields
|
import im.vector.matrix.android.internal.database.model.ChunkEntityFields
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
||||||
@ -29,18 +34,16 @@ import im.vector.matrix.android.internal.database.query.where
|
|||||||
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 io.realm.OrderedCollectionChangeSet
|
import io.realm.*
|
||||||
import io.realm.OrderedRealmCollectionChangeListener
|
|
||||||
import io.realm.Realm
|
|
||||||
import io.realm.RealmConfiguration
|
|
||||||
import io.realm.RealmQuery
|
|
||||||
import io.realm.RealmResults
|
|
||||||
import io.realm.Sort
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
|
|
||||||
private const val INITIAL_LOAD_SIZE = 30
|
private const val INITIAL_LOAD_SIZE = 20
|
||||||
|
private const val THREAD_NAME = "TIMELINE_DB_THREAD"
|
||||||
|
|
||||||
internal class DefaultTimeline(
|
internal class DefaultTimeline(
|
||||||
private val roomId: String,
|
private val roomId: String,
|
||||||
@ -54,16 +57,24 @@ internal class DefaultTimeline(
|
|||||||
) : Timeline {
|
) : Timeline {
|
||||||
|
|
||||||
override var listener: Timeline.Listener? = null
|
override var listener: Timeline.Listener? = null
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
listener?.onUpdated(snapshot())
|
||||||
|
}
|
||||||
|
|
||||||
private lateinit var realm: Realm
|
private val isStarted = AtomicBoolean(false)
|
||||||
|
private val handlerThread = AtomicReference<HandlerThread>()
|
||||||
|
private val handler = AtomicReference<Handler>()
|
||||||
|
private val realm = AtomicReference<Realm>()
|
||||||
|
|
||||||
|
private val cancelableBag = CancelableBag()
|
||||||
private lateinit var liveEvents: RealmResults<EventEntity>
|
private lateinit var liveEvents: RealmResults<EventEntity>
|
||||||
private var prevDisplayIndex: Int = 0
|
private var prevDisplayIndex: Int = 0
|
||||||
private var nextDisplayIndex: Int = 0
|
private var nextDisplayIndex: Int = 0
|
||||||
private val isLive = initialEventId == null
|
private val isLive = initialEventId == null
|
||||||
private val builtEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
|
private val builtEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
|
||||||
|
|
||||||
|
private val eventsChangeListener = OrderedRealmCollectionChangeListener<RealmResults<EventEntity>> { _, changeSet ->
|
||||||
private val changeListener = OrderedRealmCollectionChangeListener<RealmResults<EventEntity>> { _, changeSet ->
|
|
||||||
if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL) {
|
if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL) {
|
||||||
handleInitialLoad()
|
handleInitialLoad()
|
||||||
} else {
|
} else {
|
||||||
@ -78,32 +89,48 @@ internal class DefaultTimeline(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@UiThread
|
|
||||||
override fun paginate(direction: Timeline.Direction, count: Int) {
|
override fun paginate(direction: Timeline.Direction, count: Int) {
|
||||||
if (direction == Timeline.Direction.FORWARDS && isLive) {
|
handler.get()?.post {
|
||||||
return
|
if (!hasMoreToLoadLive(direction) && hasReachedEndLive(direction)) {
|
||||||
|
return@post
|
||||||
}
|
}
|
||||||
val startDisplayIndex = if (direction == Timeline.Direction.BACKWARDS) prevDisplayIndex else nextDisplayIndex
|
val startDisplayIndex = if (direction == Timeline.Direction.BACKWARDS) prevDisplayIndex else nextDisplayIndex
|
||||||
val hasBuiltCountItems = insertFromLiveResults(startDisplayIndex, direction, count.toLong())
|
val builtCountItems = insertFromLiveResults(startDisplayIndex, direction, count.toLong())
|
||||||
if (hasBuiltCountItems.not()) {
|
if (builtCountItems < count) {
|
||||||
val token = getToken(direction) ?: return
|
val limit = count - builtCountItems
|
||||||
helper.runIfNotRunning(direction.toRequestType()) {
|
val token = getTokenLive(direction) ?: return@post
|
||||||
executePaginationTask(it, token, direction.toPaginationDirection(), 30)
|
helper.runIfNotRunning(direction.toRequestType()) { executePaginationTask(it, token, direction, limit) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@UiThread
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
realm = Realm.getInstance(realmConfiguration)
|
if (isStarted.compareAndSet(false, true)) {
|
||||||
liveEvents = buildQuery(initialEventId).findAllAsync()
|
val handlerThread = HandlerThread(THREAD_NAME)
|
||||||
liveEvents.addChangeListener(changeListener)
|
handlerThread.start()
|
||||||
|
val handler = Handler(handlerThread.looper)
|
||||||
|
this.handlerThread.set(handlerThread)
|
||||||
|
this.handler.set(handler)
|
||||||
|
handler.post {
|
||||||
|
val realm = Realm.getInstance(realmConfiguration)
|
||||||
|
this.realm.set(realm)
|
||||||
|
liveEvents = buildEventQuery(realm).findAllAsync()
|
||||||
|
liveEvents.addChangeListener(eventsChangeListener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@UiThread
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
|
if (isStarted.compareAndSet(true, false)) {
|
||||||
|
handler.get()?.post {
|
||||||
|
cancelableBag.cancel()
|
||||||
liveEvents.removeAllChangeListeners()
|
liveEvents.removeAllChangeListeners()
|
||||||
realm.close()
|
realm.getAndSet(null)?.close()
|
||||||
|
handler.set(null)
|
||||||
|
handlerThread.getAndSet(null)?.quit()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun snapshot(): List<TimelineEvent> = synchronized(builtEvents) {
|
override fun snapshot(): List<TimelineEvent> = synchronized(builtEvents) {
|
||||||
@ -114,6 +141,21 @@ internal class DefaultTimeline(
|
|||||||
return builtEvents.size
|
return builtEvents.size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun hasReachedEnd(direction: Timeline.Direction): Boolean {
|
||||||
|
return handler.get()?.postAndWait {
|
||||||
|
hasReachedEndLive(direction)
|
||||||
|
} ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hasMoreToLoad(direction: Timeline.Direction): Boolean {
|
||||||
|
return handler.get()?.postAndWait {
|
||||||
|
hasMoreToLoadLive(direction)
|
||||||
|
} ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This has to be called on TimelineThread as it access realm live results
|
||||||
|
*/
|
||||||
private fun handleInitialLoad() = synchronized(builtEvents) {
|
private fun handleInitialLoad() = synchronized(builtEvents) {
|
||||||
val initialDisplayIndex = if (isLive) {
|
val initialDisplayIndex = if (isLive) {
|
||||||
liveEvents.firstOrNull()?.displayIndex
|
liveEvents.firstOrNull()?.displayIndex
|
||||||
@ -138,19 +180,22 @@ internal class DefaultTimeline(
|
|||||||
|
|
||||||
private fun executePaginationTask(requestCallback: PagingRequestHelper.Request.Callback,
|
private fun executePaginationTask(requestCallback: PagingRequestHelper.Request.Callback,
|
||||||
from: String,
|
from: String,
|
||||||
direction: PaginationDirection,
|
direction: Timeline.Direction,
|
||||||
limit: Int) {
|
limit: Int) {
|
||||||
|
|
||||||
val params = PaginationTask.Params(roomId = roomId,
|
val params = PaginationTask.Params(roomId = roomId,
|
||||||
from = from,
|
from = from,
|
||||||
direction = direction,
|
direction = direction.toPaginationDirection(),
|
||||||
limit = limit)
|
limit = limit)
|
||||||
|
|
||||||
paginationTask.configureWith(params)
|
paginationTask.configureWith(params)
|
||||||
.enableRetry()
|
.enableRetry()
|
||||||
.dispatchTo(object : MatrixCallback<Boolean> {
|
.dispatchTo(object : MatrixCallback<TokenChunkEventPersistor.Result> {
|
||||||
override fun onSuccess(data: Boolean) {
|
override fun onSuccess(data: TokenChunkEventPersistor.Result) {
|
||||||
requestCallback.recordSuccess()
|
requestCallback.recordSuccess()
|
||||||
|
if (data == TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE) {
|
||||||
|
paginate(direction, limit)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
override fun onFailure(failure: Throwable) {
|
||||||
@ -158,26 +203,63 @@ internal class DefaultTimeline(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.executeBy(taskExecutor)
|
.executeBy(taskExecutor)
|
||||||
|
.addTo(cancelableBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getToken(direction: Timeline.Direction): String? {
|
/**
|
||||||
val chunkEntity = liveEvents.firstOrNull()?.chunk?.firstOrNull() ?: return null
|
* This has to be called on TimelineThread as it access realm live results
|
||||||
|
*/
|
||||||
|
private fun getTokenLive(direction: Timeline.Direction): String? {
|
||||||
|
val chunkEntity = getLiveChunk() ?: return null
|
||||||
return if (direction == Timeline.Direction.BACKWARDS) chunkEntity.prevToken else chunkEntity.nextToken
|
return if (direction == Timeline.Direction.BACKWARDS) chunkEntity.prevToken else chunkEntity.nextToken
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This has to be called on MonarchyThread as it access realm live results
|
* This has to be called on TimelineThread as it access realm live results
|
||||||
* @return true if count items has been added
|
*/
|
||||||
|
private fun hasReachedEndLive(direction: Timeline.Direction): Boolean {
|
||||||
|
val liveChunk = getLiveChunk() ?: return false
|
||||||
|
return if (direction == Timeline.Direction.FORWARDS) {
|
||||||
|
liveChunk.isLastForward
|
||||||
|
} else {
|
||||||
|
liveChunk.isLastBackward || liveEvents.lastOrNull()?.type == EventType.STATE_ROOM_CREATE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This has to be called on TimelineThread as it access realm live results
|
||||||
|
*/
|
||||||
|
private fun hasMoreToLoadLive(direction: Timeline.Direction): Boolean {
|
||||||
|
if (liveEvents.isEmpty()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return if (direction == Timeline.Direction.FORWARDS) {
|
||||||
|
builtEvents.firstOrNull()?.displayIndex != liveEvents.firstOrNull()?.displayIndex
|
||||||
|
} else {
|
||||||
|
builtEvents.lastOrNull()?.displayIndex != liveEvents.lastOrNull()?.displayIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This has to be called on TimelineThread as it access realm live results
|
||||||
|
*/
|
||||||
|
private fun getLiveChunk(): ChunkEntity? {
|
||||||
|
return liveEvents.firstOrNull()?.chunk?.firstOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This has to be called on TimelineThread as it access realm live results
|
||||||
|
* @return number of items who have been added
|
||||||
*/
|
*/
|
||||||
private fun insertFromLiveResults(startDisplayIndex: Int,
|
private fun insertFromLiveResults(startDisplayIndex: Int,
|
||||||
direction: Timeline.Direction,
|
direction: Timeline.Direction,
|
||||||
count: Long): Boolean = synchronized(builtEvents) {
|
count: Long): Int = synchronized(builtEvents) {
|
||||||
if (count < 1) {
|
if (count < 1) {
|
||||||
throw java.lang.IllegalStateException("You should provide a count superior to 0")
|
throw java.lang.IllegalStateException("You should provide a count superior to 0")
|
||||||
}
|
}
|
||||||
val offsetResults = getOffsetResults(startDisplayIndex, direction, count)
|
val offsetResults = getOffsetResults(startDisplayIndex, direction, count)
|
||||||
if (offsetResults.isEmpty()) {
|
if (offsetResults.isEmpty()) {
|
||||||
return false
|
return 0
|
||||||
}
|
}
|
||||||
val offsetIndex = offsetResults.last()!!.displayIndex
|
val offsetIndex = offsetResults.last()!!.displayIndex
|
||||||
if (direction == Timeline.Direction.BACKWARDS) {
|
if (direction == Timeline.Direction.BACKWARDS) {
|
||||||
@ -191,9 +273,12 @@ internal class DefaultTimeline(
|
|||||||
builtEvents.add(position, timelineEvent)
|
builtEvents.add(position, timelineEvent)
|
||||||
}
|
}
|
||||||
listener?.onUpdated(snapshot())
|
listener?.onUpdated(snapshot())
|
||||||
return offsetResults.size.toLong() == count
|
return offsetResults.size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This has to be called on TimelineThread as it access realm live results
|
||||||
|
*/
|
||||||
private fun getOffsetResults(startDisplayIndex: Int,
|
private fun getOffsetResults(startDisplayIndex: Int,
|
||||||
direction: Timeline.Direction,
|
direction: Timeline.Direction,
|
||||||
count: Long): RealmResults<EventEntity> {
|
count: Long): RealmResults<EventEntity> {
|
||||||
@ -210,21 +295,33 @@ internal class DefaultTimeline(
|
|||||||
return offsetQuery.limit(count).findAll()
|
return offsetQuery.limit(count).findAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildQuery(eventId: String?): RealmQuery<EventEntity> {
|
private fun buildEventQuery(realm: Realm): RealmQuery<EventEntity> {
|
||||||
val query = if (eventId == null) {
|
val query = if (initialEventId == null) {
|
||||||
EventEntity
|
EventEntity
|
||||||
.where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.LINKED_ONLY)
|
.where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.LINKED_ONLY)
|
||||||
.equalTo("${EventEntityFields.CHUNK}.${ChunkEntityFields.IS_LAST}", true)
|
.equalTo("${EventEntityFields.CHUNK}.${ChunkEntityFields.IS_LAST_FORWARD}", true)
|
||||||
} else {
|
} else {
|
||||||
EventEntity
|
EventEntity
|
||||||
.where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.BOTH)
|
.where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.BOTH)
|
||||||
.`in`("${EventEntityFields.CHUNK}.${ChunkEntityFields.EVENTS.EVENT_ID}", arrayOf(eventId))
|
.`in`("${EventEntityFields.CHUNK}.${ChunkEntityFields.EVENTS.EVENT_ID}", arrayOf(initialEventId))
|
||||||
}
|
}
|
||||||
query.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
|
query.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
|
||||||
return query
|
return query
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun <T> Handler.postAndWait(runnable: () -> T): T {
|
||||||
|
val lock = CountDownLatch(1)
|
||||||
|
val atomicReference = AtomicReference<T>()
|
||||||
|
post {
|
||||||
|
val result = runnable()
|
||||||
|
atomicReference.set(result)
|
||||||
|
lock.countDown()
|
||||||
|
}
|
||||||
|
lock.await()
|
||||||
|
return atomicReference.get()
|
||||||
|
}
|
||||||
|
|
||||||
private fun Timeline.Direction.toRequestType(): PagingRequestHelper.RequestType {
|
private fun Timeline.Direction.toRequestType(): PagingRequestHelper.RequestType {
|
||||||
return if (this == Timeline.Direction.BACKWARDS) PagingRequestHelper.RequestType.BEFORE else PagingRequestHelper.RequestType.AFTER
|
return if (this == Timeline.Direction.BACKWARDS) PagingRequestHelper.RequestType.BEFORE else PagingRequestHelper.RequestType.AFTER
|
||||||
}
|
}
|
||||||
@ -233,4 +330,5 @@ internal class DefaultTimeline(
|
|||||||
private fun Timeline.Direction.toPaginationDirection(): PaginationDirection {
|
private fun Timeline.Direction.toPaginationDirection(): PaginationDirection {
|
||||||
return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS
|
return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -16,12 +16,9 @@
|
|||||||
|
|
||||||
package im.vector.matrix.android.internal.session.room.timeline
|
package im.vector.matrix.android.internal.session.room.timeline
|
||||||
|
|
||||||
import androidx.paging.PagedList
|
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEventInterceptor
|
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineService
|
import im.vector.matrix.android.api.session.room.timeline.TimelineService
|
||||||
import im.vector.matrix.android.internal.database.model.ChunkEntityFields
|
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
@ -29,12 +26,7 @@ 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 im.vector.matrix.android.internal.util.tryTransactionAsync
|
import im.vector.matrix.android.internal.util.tryTransactionAsync
|
||||||
import io.realm.Realm
|
|
||||||
import io.realm.RealmQuery
|
|
||||||
import io.realm.Sort
|
|
||||||
|
|
||||||
private const val PAGE_SIZE = 100
|
|
||||||
private const val PREFETCH_DISTANCE = 30
|
|
||||||
private const val EVENT_NOT_FOUND_INDEX = -1
|
private const val EVENT_NOT_FOUND_INDEX = -1
|
||||||
|
|
||||||
internal class DefaultTimelineService(private val roomId: String,
|
internal class DefaultTimelineService(private val roomId: String,
|
||||||
@ -46,8 +38,6 @@ internal class DefaultTimelineService(private val roomId: String,
|
|||||||
private val helper: PagingRequestHelper
|
private val helper: PagingRequestHelper
|
||||||
) : TimelineService {
|
) : TimelineService {
|
||||||
|
|
||||||
private val eventInterceptors = ArrayList<TimelineEventInterceptor>()
|
|
||||||
|
|
||||||
override fun createTimeline(eventId: String?): Timeline {
|
override fun createTimeline(eventId: String?): Timeline {
|
||||||
return DefaultTimeline(roomId, eventId, monarchy.realmConfiguration, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask, helper)
|
return DefaultTimeline(roomId, eventId, monarchy.realmConfiguration, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask, helper)
|
||||||
}
|
}
|
||||||
@ -73,15 +63,6 @@ internal class DefaultTimelineService(private val roomId: String,
|
|||||||
contextOfEventTask.configureWith(params).executeBy(taskExecutor)
|
contextOfEventTask.configureWith(params).executeBy(taskExecutor)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildPagedListConfig(): PagedList.Config {
|
|
||||||
return PagedList.Config.Builder()
|
|
||||||
.setEnablePlaceholders(false)
|
|
||||||
.setPageSize(PAGE_SIZE)
|
|
||||||
.setInitialLoadSizeHint(2 * PAGE_SIZE)
|
|
||||||
.setPrefetchDistance(PREFETCH_DISTANCE)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun clearUnlinkedEvents() {
|
private fun clearUnlinkedEvents() {
|
||||||
monarchy.tryTransactionAsync { realm ->
|
monarchy.tryTransactionAsync { realm ->
|
||||||
val unlinkedEvents = EventEntity
|
val unlinkedEvents = EventEntity
|
||||||
@ -101,18 +82,5 @@ internal class DefaultTimelineService(private val roomId: String,
|
|||||||
return displayIndex
|
return displayIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildDataSourceFactoryQuery(realm: Realm, eventId: String?): RealmQuery<EventEntity> {
|
|
||||||
val query = if (eventId == null) {
|
|
||||||
EventEntity
|
|
||||||
.where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.LINKED_ONLY)
|
|
||||||
.equalTo("${EventEntityFields.CHUNK}.${ChunkEntityFields.IS_LAST}", true)
|
|
||||||
} else {
|
|
||||||
EventEntity
|
|
||||||
.where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.BOTH)
|
|
||||||
.`in`("${EventEntityFields.CHUNK}.${ChunkEntityFields.EVENTS.EVENT_ID}", arrayOf(eventId))
|
|
||||||
}
|
|
||||||
return query.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -95,8 +95,8 @@ internal class TimelineBoundaryCallback(private val roomId: String,
|
|||||||
|
|
||||||
paginationTask.configureWith(params)
|
paginationTask.configureWith(params)
|
||||||
.enableRetry()
|
.enableRetry()
|
||||||
.dispatchTo(object : MatrixCallback<Boolean> {
|
.dispatchTo(object : MatrixCallback<TokenChunkEventPersistor.Result> {
|
||||||
override fun onSuccess(data: Boolean) {
|
override fun onSuccess(data: TokenChunkEventPersistor.Result) {
|
||||||
requestCallback.recordSuccess()
|
requestCallback.recordSuccess()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,13 +36,15 @@ import io.realm.kotlin.createObject
|
|||||||
|
|
||||||
internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {
|
internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {
|
||||||
|
|
||||||
|
enum class Result {
|
||||||
|
SHOULD_FETCH_MORE,
|
||||||
|
SUCCESS
|
||||||
|
}
|
||||||
|
|
||||||
fun insertInDb(receivedChunk: TokenChunkEvent,
|
fun insertInDb(receivedChunk: TokenChunkEvent,
|
||||||
roomId: String,
|
roomId: String,
|
||||||
direction: PaginationDirection): Try<Boolean> {
|
direction: PaginationDirection): Try<Result> {
|
||||||
|
|
||||||
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,7 +73,9 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {
|
|||||||
nextChunk?.apply { this.prevToken = prevToken }
|
nextChunk?.apply { this.prevToken = prevToken }
|
||||||
?: ChunkEntity.create(realm, prevToken, nextToken)
|
?: ChunkEntity.create(realm, prevToken, nextToken)
|
||||||
}
|
}
|
||||||
|
if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) {
|
||||||
|
currentChunk.isLastBackward = true
|
||||||
|
} else {
|
||||||
currentChunk.addAll(roomId, receivedChunk.events, direction, isUnlinked = currentChunk.isUnlinked())
|
currentChunk.addAll(roomId, receivedChunk.events, direction, isUnlinked = currentChunk.isUnlinked())
|
||||||
|
|
||||||
// Then we merge chunks if needed
|
// Then we merge chunks if needed
|
||||||
@ -91,7 +95,14 @@ 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 }
|
}
|
||||||
|
.map {
|
||||||
|
if (receivedChunk.events.isEmpty() && receivedChunk.stateEvents.isEmpty() && receivedChunk.start != receivedChunk.end) {
|
||||||
|
Result.SHOULD_FETCH_MORE
|
||||||
|
} else {
|
||||||
|
Result.SUCCESS
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleMerge(roomEntity: RoomEntity,
|
private fun handleMerge(roomEntity: RoomEntity,
|
||||||
|
@ -51,7 +51,6 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
|
|||||||
data class INVITED(val data: Map<String, InvitedRoomSync>) : HandlingStrategy()
|
data class INVITED(val data: Map<String, InvitedRoomSync>) : HandlingStrategy()
|
||||||
data class LEFT(val data: Map<String, RoomSync>) : HandlingStrategy()
|
data class LEFT(val data: Map<String, RoomSync>) : HandlingStrategy()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handle(roomsSyncResponse: RoomsSyncResponse) {
|
fun handle(roomsSyncResponse: RoomsSyncResponse) {
|
||||||
monarchy.runTransactionSync { realm ->
|
monarchy.runTransactionSync { realm ->
|
||||||
handleRoomSync(realm, RoomSyncHandler.HandlingStrategy.JOINED(roomsSyncResponse.join))
|
handleRoomSync(realm, RoomSyncHandler.HandlingStrategy.JOINED(roomsSyncResponse.join))
|
||||||
@ -164,8 +163,8 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
|
|||||||
realm.createObject<ChunkEntity>().apply { this.prevToken = prevToken }
|
realm.createObject<ChunkEntity>().apply { this.prevToken = prevToken }
|
||||||
}
|
}
|
||||||
|
|
||||||
lastChunk?.isLast = false
|
lastChunk?.isLastForward = false
|
||||||
chunkEntity.isLast = true
|
chunkEntity.isLastForward = true
|
||||||
chunkEntity.addAll(roomId, eventList, PaginationDirection.FORWARDS, stateIndexOffset)
|
chunkEntity.addAll(roomId, eventList, PaginationDirection.FORWARDS, stateIndexOffset)
|
||||||
return chunkEntity
|
return chunkEntity
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user