forked from GitHub-Mirror/riotX-android
Timeline rework : first version working for backward navigation (need more testing)
This commit is contained in:
parent
820709d433
commit
c12bc5e02d
@ -22,6 +22,7 @@ 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, val index: Int) : RoomDetailActions()
|
data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions()
|
||||||
|
object LoadMore: RoomDetailActions()
|
||||||
|
|
||||||
}
|
}
|
@ -24,7 +24,6 @@ import android.view.ViewGroup
|
|||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
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.Success
|
|
||||||
import com.airbnb.mvrx.fragmentViewModel
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
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
|
||||||
@ -36,6 +35,7 @@ import im.vector.riotredesign.features.home.HomeModule
|
|||||||
import im.vector.riotredesign.features.home.HomePermalinkHandler
|
import im.vector.riotredesign.features.home.HomePermalinkHandler
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.animation.TimelineItemAnimator
|
import im.vector.riotredesign.features.home.room.detail.timeline.animation.TimelineItemAnimator
|
||||||
|
import im.vector.riotredesign.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener
|
||||||
import im.vector.riotredesign.features.media.MediaContentRenderer
|
import im.vector.riotredesign.features.media.MediaContentRenderer
|
||||||
import im.vector.riotredesign.features.media.MediaViewerActivity
|
import im.vector.riotredesign.features.media.MediaViewerActivity
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
@ -80,7 +80,6 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
|
|||||||
setupRecyclerView()
|
setupRecyclerView()
|
||||||
setupToolbar()
|
setupToolbar()
|
||||||
setupSendButton()
|
setupSendButton()
|
||||||
timelineEventController.requestModelBuild()
|
|
||||||
roomDetailViewModel.subscribe { renderState(it) }
|
roomDetailViewModel.subscribe { renderState(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,6 +110,11 @@ 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) {
|
||||||
|
override fun onLoadMore(page: Int, totalItemsCount: Int) {
|
||||||
|
roomDetailViewModel.process(RoomDetailActions.LoadMore)
|
||||||
|
}
|
||||||
|
})
|
||||||
recyclerView.setController(timelineEventController)
|
recyclerView.setController(timelineEventController)
|
||||||
timelineEventController.callback = this
|
timelineEventController.callback = this
|
||||||
}
|
}
|
||||||
@ -119,29 +123,16 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
|
|||||||
sendButton.setOnClickListener {
|
sendButton.setOnClickListener {
|
||||||
val textMessage = composerEditText.text.toString()
|
val textMessage = composerEditText.text.toString()
|
||||||
if (textMessage.isNotBlank()) {
|
if (textMessage.isNotBlank()) {
|
||||||
composerEditText.text = null
|
|
||||||
roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage))
|
roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage))
|
||||||
|
composerEditText.text = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderState(state: RoomDetailViewState) {
|
private fun renderState(state: RoomDetailViewState) {
|
||||||
renderRoomSummary(state)
|
renderRoomSummary(state)
|
||||||
renderTimeline(state)
|
timelineEventController.setTimeline(state.timeline)
|
||||||
}
|
//renderTimeline(state)
|
||||||
|
|
||||||
private fun renderTimeline(state: RoomDetailViewState) {
|
|
||||||
when (state.asyncTimelineData) {
|
|
||||||
is Success -> {
|
|
||||||
val timelineData = state.asyncTimelineData()
|
|
||||||
val lockAutoScroll = timelineData?.let {
|
|
||||||
it.events == timelineEventController.currentList && it.isLoadingForward
|
|
||||||
} ?: true
|
|
||||||
|
|
||||||
scrollOnNewMessageCallback.isLocked.set(lockAutoScroll)
|
|
||||||
timelineEventController.update(timelineData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderRoomSummary(state: RoomDetailViewState) {
|
private fun renderRoomSummary(state: RoomDetailViewState) {
|
||||||
@ -163,8 +154,8 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
|
|||||||
homePermalinkHandler.launch(url)
|
homePermalinkHandler.launch(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onEventVisible(event: TimelineEvent, index: Int) {
|
override fun onEventVisible(event: TimelineEvent) {
|
||||||
roomDetailViewModel.process(RoomDetailActions.EventDisplayed(event, index))
|
roomDetailViewModel.process(RoomDetailActions.EventDisplayed(event))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMediaClicked(mediaData: MediaContentRenderer.Data, view: View) {
|
override fun onMediaClicked(mediaData: MediaContentRenderer.Data, view: View) {
|
||||||
|
@ -22,8 +22,8 @@ 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.extensions.lastMinBy
|
|
||||||
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
|
||||||
@ -38,8 +38,8 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
|||||||
private val room = session.getRoom(initialState.roomId)!!
|
private val room = session.getRoom(initialState.roomId)!!
|
||||||
private val roomId = initialState.roomId
|
private val roomId = initialState.roomId
|
||||||
private val eventId = initialState.eventId
|
private val eventId = initialState.eventId
|
||||||
|
|
||||||
private val displayedEventsObservable = BehaviorRelay.create<RoomDetailActions.EventDisplayed>()
|
private val displayedEventsObservable = BehaviorRelay.create<RoomDetailActions.EventDisplayed>()
|
||||||
|
private val timeline = room.createTimeline(eventId)
|
||||||
|
|
||||||
companion object : MvRxViewModelFactory<RoomDetailViewModel, RoomDetailViewState> {
|
companion object : MvRxViewModelFactory<RoomDetailViewModel, RoomDetailViewState> {
|
||||||
|
|
||||||
@ -53,9 +53,10 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
observeRoomSummary()
|
observeRoomSummary()
|
||||||
observeTimeline()
|
|
||||||
observeDisplayedEvents()
|
observeDisplayedEvents()
|
||||||
room.loadRoomMembersIfNeeded()
|
room.loadRoomMembersIfNeeded()
|
||||||
|
timeline.start()
|
||||||
|
setState { copy(timeline = this@RoomDetailViewModel.timeline) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun process(action: RoomDetailActions) {
|
fun process(action: RoomDetailActions) {
|
||||||
@ -63,6 +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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,11 +85,11 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
|||||||
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.
|
||||||
displayedEventsObservable.hide()
|
displayedEventsObservable
|
||||||
.buffer(1, TimeUnit.SECONDS)
|
.buffer(1, TimeUnit.SECONDS)
|
||||||
.filter { it.isNotEmpty() }
|
.filter { it.isNotEmpty() }
|
||||||
.subscribeBy(onNext = { actions ->
|
.subscribeBy(onNext = { actions ->
|
||||||
val mostRecentEvent = actions.lastMinBy { it.index }
|
val mostRecentEvent = actions.maxBy { it.event.displayIndex }
|
||||||
mostRecentEvent?.event?.root?.eventId?.let { eventId ->
|
mostRecentEvent?.event?.root?.eventId?.let { eventId ->
|
||||||
room.setReadReceipt(eventId, callback = object : MatrixCallback<Void> {})
|
room.setReadReceipt(eventId, callback = object : MatrixCallback<Void> {})
|
||||||
}
|
}
|
||||||
@ -102,12 +104,9 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeTimeline() {
|
override fun onCleared() {
|
||||||
room.rx().timeline(eventId)
|
super.onCleared()
|
||||||
.execute { timelineData ->
|
timeline.dispose()
|
||||||
copy(asyncTimelineData = timelineData)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -20,11 +20,13 @@ import com.airbnb.mvrx.Async
|
|||||||
import com.airbnb.mvrx.MvRxState
|
import com.airbnb.mvrx.MvRxState
|
||||||
import com.airbnb.mvrx.Uninitialized
|
import com.airbnb.mvrx.Uninitialized
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||||
|
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineData
|
import im.vector.matrix.android.api.session.room.timeline.TimelineData
|
||||||
|
|
||||||
data class RoomDetailViewState(
|
data class RoomDetailViewState(
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
val eventId: String?,
|
val eventId: String?,
|
||||||
|
val timeline: Timeline? = null,
|
||||||
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
|
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
|
||||||
val asyncTimelineData: Async<TimelineData> = Uninitialized
|
val asyncTimelineData: Async<TimelineData> = Uninitialized
|
||||||
) : MvRxState {
|
) : MvRxState {
|
||||||
|
@ -25,7 +25,7 @@ class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager)
|
|||||||
var isLocked = AtomicBoolean(true)
|
var isLocked = AtomicBoolean(true)
|
||||||
|
|
||||||
override fun onInserted(position: Int, count: Int) {
|
override fun onInserted(position: Int, count: Int) {
|
||||||
if (isLocked.compareAndSet(false, true) && position == 0 && layoutManager.findFirstVisibleItemPosition() == 0) {
|
if (position == 0 && layoutManager.findFirstVisibleItemPosition() == 0) {
|
||||||
layoutManager.scrollToPosition(0)
|
layoutManager.scrollToPosition(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,44 +17,79 @@
|
|||||||
package im.vector.riotredesign.features.home.room.detail.timeline
|
package im.vector.riotredesign.features.home.room.detail.timeline
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.ListUpdateCallback
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.airbnb.epoxy.EpoxyAsyncUtil
|
import com.airbnb.epoxy.EpoxyAsyncUtil
|
||||||
|
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.events.model.EventType
|
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineData
|
|
||||||
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.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.TimelineMediaSizeProvider
|
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.DaySeparatorItem_
|
import im.vector.riotredesign.features.home.room.detail.timeline.item.DaySeparatorItem_
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.paging.PagedListEpoxyController
|
|
||||||
import im.vector.riotredesign.features.media.MediaContentRenderer
|
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
|
||||||
) : PagedListEpoxyController<TimelineEvent>(
|
) : EpoxyController(
|
||||||
EpoxyAsyncUtil.getAsyncBackgroundHandler(),
|
EpoxyAsyncUtil.getAsyncBackgroundHandler(),
|
||||||
EpoxyAsyncUtil.getAsyncBackgroundHandler()
|
EpoxyAsyncUtil.getAsyncBackgroundHandler()
|
||||||
) {
|
), Timeline.Listener {
|
||||||
|
|
||||||
|
private val modelCache = arrayListOf<List<EpoxyModel<*>>>()
|
||||||
|
private var currentSnapshot: List<TimelineEvent> = emptyList()
|
||||||
|
|
||||||
|
override fun onUpdated(snapshot: List<TimelineEvent>) {
|
||||||
|
submitSnapshot(snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val listUpdateCallback = object : ListUpdateCallback {
|
||||||
|
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
||||||
|
(position until (position + count)).forEach {
|
||||||
|
modelCache[it] = emptyList()
|
||||||
|
}
|
||||||
|
requestModelBuild()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
||||||
|
//no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onInserted(position: Int, count: Int) {
|
||||||
|
if (modelCache.isNotEmpty() && position == modelCache.size) {
|
||||||
|
modelCache[position - 1] = emptyList()
|
||||||
|
}
|
||||||
|
(0 until count).forEach {
|
||||||
|
modelCache.add(position, emptyList())
|
||||||
|
}
|
||||||
|
requestModelBuild()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRemoved(position: Int, count: Int) {
|
||||||
|
//no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private var isLoadingForward: Boolean = false
|
private var isLoadingForward: Boolean = false
|
||||||
private var isLoadingBackward: Boolean = false
|
private var isLoadingBackward: Boolean = false
|
||||||
private var hasReachedEnd: Boolean = true
|
private var hasReachedEnd: Boolean = true
|
||||||
|
|
||||||
|
private var timeline: Timeline? = null
|
||||||
var callback: Callback? = null
|
var callback: Callback? = null
|
||||||
|
|
||||||
fun update(timelineData: TimelineData?) {
|
fun setTimeline(timeline: Timeline?) {
|
||||||
timelineData?.let {
|
if (this.timeline != timeline) {
|
||||||
isLoadingForward = it.isLoadingForward
|
this.timeline = timeline
|
||||||
isLoadingBackward = it.isLoadingBackward
|
this.timeline?.listener = this
|
||||||
hasReachedEnd = it.events.lastOrNull()?.root?.type == EventType.STATE_ROOM_CREATE
|
submitSnapshot(timeline?.snapshot() ?: emptyList())
|
||||||
submitList(it.events)
|
|
||||||
requestModelBuild()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,12 +98,22 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||||||
timelineMediaSizeProvider.recyclerView = recyclerView
|
timelineMediaSizeProvider.recyclerView = recyclerView
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun buildItemModels(currentPosition: Int, items: List<TimelineEvent?>): List<EpoxyModel<*>> {
|
override fun buildModels() {
|
||||||
if (items.isNullOrEmpty()) {
|
add(getModels())
|
||||||
return emptyList()
|
}
|
||||||
|
|
||||||
|
private fun getModels(): List<EpoxyModel<*>> {
|
||||||
|
(0 until modelCache.size).forEach { position ->
|
||||||
|
if (modelCache[position].isEmpty()) {
|
||||||
|
modelCache[position] = buildItemModels(position, currentSnapshot)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return modelCache.flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildItemModels(currentPosition: Int, items: List<TimelineEvent>): List<EpoxyModel<*>> {
|
||||||
val epoxyModels = ArrayList<EpoxyModel<*>>()
|
val epoxyModels = ArrayList<EpoxyModel<*>>()
|
||||||
val event = items[currentPosition] ?: return emptyList()
|
val event = items[currentPosition]
|
||||||
val nextEvent = if (currentPosition + 1 < items.size) items[currentPosition + 1] else null
|
val nextEvent = if (currentPosition + 1 < items.size) items[currentPosition + 1] else null
|
||||||
|
|
||||||
val date = event.root.localDateTime()
|
val date = event.root.localDateTime()
|
||||||
@ -77,7 +122,7 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||||||
|
|
||||||
timelineItemFactory.create(event, nextEvent, callback).also {
|
timelineItemFactory.create(event, nextEvent, callback).also {
|
||||||
it.id(event.localId)
|
it.id(event.localId)
|
||||||
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event, currentPosition))
|
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
|
||||||
epoxyModels.add(it)
|
epoxyModels.add(it)
|
||||||
}
|
}
|
||||||
if (addDaySeparator) {
|
if (addDaySeparator) {
|
||||||
@ -88,21 +133,17 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||||||
return epoxyModels
|
return epoxyModels
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addModels(models: List<EpoxyModel<*>>) {
|
private fun submitSnapshot(newSnapshot: List<TimelineEvent>) {
|
||||||
LoadingItemModel_()
|
EpoxyAsyncUtil.getAsyncBackgroundHandler().post {
|
||||||
.id("forward_loading_item")
|
val diffCallback = TimelineEventDiffUtilCallback(currentSnapshot, newSnapshot)
|
||||||
.addIf(isLoadingForward, this)
|
currentSnapshot = newSnapshot
|
||||||
|
val diffResult = DiffUtil.calculateDiff(diffCallback)
|
||||||
super.add(models)
|
diffResult.dispatchUpdatesTo(listUpdateCallback)
|
||||||
|
}
|
||||||
LoadingItemModel_()
|
|
||||||
.id("backward_loading_item")
|
|
||||||
.addIf(!hasReachedEnd, this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
fun onEventVisible(event: TimelineEvent, index: Int)
|
fun onEventVisible(event: TimelineEvent)
|
||||||
fun onUrlClicked(url: String)
|
fun onUrlClicked(url: String)
|
||||||
fun onMediaClicked(mediaData: MediaContentRenderer.Data, view: View)
|
fun onMediaClicked(mediaData: MediaContentRenderer.Data, view: View)
|
||||||
}
|
}
|
||||||
@ -110,13 +151,12 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private class TimelineEventVisibilityStateChangedListener(private val callback: TimelineEventController.Callback?,
|
private class TimelineEventVisibilityStateChangedListener(private val callback: TimelineEventController.Callback?,
|
||||||
private val event: TimelineEvent,
|
private val event: TimelineEvent)
|
||||||
private val currentPosition: Int)
|
|
||||||
: RiotEpoxyModel.OnVisibilityStateChangedListener {
|
: RiotEpoxyModel.OnVisibilityStateChangedListener {
|
||||||
|
|
||||||
override fun onVisibilityStateChanged(visibilityState: Int) {
|
override fun onVisibilityStateChanged(visibilityState: Int) {
|
||||||
if (visibilityState == VisibilityState.VISIBLE) {
|
if (visibilityState == VisibilityState.VISIBLE) {
|
||||||
callback?.onEventVisible(event, currentPosition)
|
callback?.onEventVisible(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,147 @@
|
|||||||
|
/*
|
||||||
|
* 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 androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
// Todo rework that, it has been copy/paste at the moment
|
||||||
|
public abstract class EndlessRecyclerViewScrollListener extends RecyclerView.OnScrollListener {
|
||||||
|
// Sets the starting page index
|
||||||
|
private static final int startingPageIndex = 0;
|
||||||
|
// The minimum amount of items to have below your current scroll position
|
||||||
|
// before loading more.
|
||||||
|
private int visibleThreshold = 30;
|
||||||
|
// 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
|
||||||
|
private int previousTotalItemCount = 0;
|
||||||
|
// True if we are still waiting for the last set of data to load.
|
||||||
|
private boolean loading = true;
|
||||||
|
private LinearLayoutManager mLayoutManager;
|
||||||
|
private LoadOnScrollDirection mDirection;
|
||||||
|
|
||||||
|
public EndlessRecyclerViewScrollListener(LinearLayoutManager layoutManager, LoadOnScrollDirection direction) {
|
||||||
|
this.mLayoutManager = layoutManager;
|
||||||
|
mDirection = direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// This happens many times a second during a scroll, so be wary of the code you place here.
|
||||||
|
// We are given a few useful parameters to help us work out if we need to load some more data,
|
||||||
|
// but first we check if we are waiting for the previous load to finish.
|
||||||
|
@Override
|
||||||
|
public void onScrolled(@NonNull RecyclerView view, int dx, int dy) {
|
||||||
|
int lastVisibleItemPosition = 0;
|
||||||
|
int firstVisibleItemPosition = 0;
|
||||||
|
int totalItemCount = mLayoutManager.getItemCount();
|
||||||
|
|
||||||
|
lastVisibleItemPosition = mLayoutManager.findLastVisibleItemPosition();
|
||||||
|
firstVisibleItemPosition = mLayoutManager.findFirstVisibleItemPosition();
|
||||||
|
|
||||||
|
switch (mDirection) {
|
||||||
|
case BOTTOM:
|
||||||
|
// 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
|
||||||
|
if (totalItemCount < previousTotalItemCount) {
|
||||||
|
this.currentPage = startingPageIndex;
|
||||||
|
this.previousTotalItemCount = totalItemCount;
|
||||||
|
if (totalItemCount == 0) {
|
||||||
|
this.loading = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If it’s still loading, we check to see if the dataset count has
|
||||||
|
// changed, if so we conclude it has finished loading and update the current page
|
||||||
|
// number and total item count.
|
||||||
|
if (loading && (totalItemCount > previousTotalItemCount)) {
|
||||||
|
loading = false;
|
||||||
|
previousTotalItemCount = totalItemCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it isn’t currently loading, we check to see if we have breached
|
||||||
|
// the visibleThreshold and need to reload more 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
|
||||||
|
if (!loading && (lastVisibleItemPosition + visibleThreshold) > totalItemCount) {
|
||||||
|
currentPage++;
|
||||||
|
onLoadMore(currentPage, totalItemCount);
|
||||||
|
loading = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case TOP:
|
||||||
|
// 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
|
||||||
|
if (totalItemCount < previousTotalItemCount) {
|
||||||
|
this.currentPage = startingPageIndex;
|
||||||
|
this.previousTotalItemCount = totalItemCount;
|
||||||
|
if (totalItemCount == 0) {
|
||||||
|
this.loading = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If it’s still loading, we check to see if the dataset count has
|
||||||
|
// changed, if so we conclude it has finished loading and update the current page
|
||||||
|
// number and total item count.
|
||||||
|
if (loading && (totalItemCount > previousTotalItemCount)) {
|
||||||
|
loading = false;
|
||||||
|
previousTotalItemCount = totalItemCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it isn’t currently loading, we check to see if we have breached
|
||||||
|
// the visibleThreshold and need to reload more 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
|
||||||
|
if (!loading && firstVisibleItemPosition < visibleThreshold) {
|
||||||
|
currentPage++;
|
||||||
|
onLoadMore(currentPage, totalItemCount);
|
||||||
|
loading = true;
|
||||||
|
}
|
||||||
|
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
|
||||||
|
public abstract void onLoadMore(int page, int totalItemsCount);
|
||||||
|
|
||||||
|
public enum LoadOnScrollDirection {
|
||||||
|
TOP, BOTTOM
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* 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 androidx.recyclerview.widget.DiffUtil
|
||||||
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
|
|
||||||
|
class TimelineEventDiffUtilCallback(private val oldList: List<TimelineEvent>,
|
||||||
|
private val newList: List<TimelineEvent>) : DiffUtil.Callback() {
|
||||||
|
|
||||||
|
override fun getOldListSize(): Int {
|
||||||
|
return oldList.size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getNewListSize(): Int {
|
||||||
|
return newList.size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||||
|
val oldItem = oldList[oldItemPosition]
|
||||||
|
val newItem = newList[newItemPosition]
|
||||||
|
return oldItem.localId == newItem.localId
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||||
|
val oldItem = oldList[oldItemPosition]
|
||||||
|
val newItem = newList[newItemPosition]
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
}
|
@ -1,129 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.paging
|
|
||||||
|
|
||||||
import androidx.paging.PagedList
|
|
||||||
import android.os.Handler
|
|
||||||
import androidx.recyclerview.widget.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
|
|
||||||
)
|
|
||||||
|
|
||||||
var currentList: PagedList<T>? = null
|
|
||||||
private set
|
|
||||||
|
|
||||||
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>?) {
|
|
||||||
currentList = newList
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,149 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.paging
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.os.Handler
|
|
||||||
import androidx.paging.AsyncPagedListDiffer
|
|
||||||
import androidx.paging.PagedList
|
|
||||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import androidx.recyclerview.widget.ListUpdateCallback
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
private val asyncDiffer = 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()
|
|
||||||
)
|
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -18,7 +18,6 @@ package im.vector.matrix.rx
|
|||||||
|
|
||||||
import im.vector.matrix.android.api.session.room.Room
|
import im.vector.matrix.android.api.session.room.Room
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineData
|
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
|
|
||||||
class RxRoom(private val room: Room) {
|
class RxRoom(private val room: Room) {
|
||||||
@ -27,10 +26,6 @@ class RxRoom(private val room: Room) {
|
|||||||
return room.roomSummary.asObservable()
|
return room.roomSummary.asObservable()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun timeline(eventId: String? = null): Observable<TimelineData> {
|
|
||||||
return room.timeline(eventId).asObservable()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Room.rx(): RxRoom {
|
fun Room.rx(): RxRoom {
|
||||||
|
@ -31,7 +31,6 @@ import im.vector.matrix.android.internal.util.PagingRequestHelper
|
|||||||
import im.vector.matrix.android.testCoroutineDispatchers
|
import im.vector.matrix.android.testCoroutineDispatchers
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
import io.realm.RealmConfiguration
|
import io.realm.RealmConfiguration
|
||||||
import org.amshove.kluent.shouldEqual
|
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
@ -20,13 +20,16 @@ package im.vector.matrix.android.api.session.room.timeline
|
|||||||
|
|
||||||
interface Timeline {
|
interface Timeline {
|
||||||
|
|
||||||
|
var listener: Timeline.Listener?
|
||||||
|
|
||||||
|
fun size(): Int
|
||||||
|
fun snapshot(): List<TimelineEvent>
|
||||||
fun paginate(direction: Direction, count: Int)
|
fun paginate(direction: Direction, count: Int)
|
||||||
fun addListener(listener: Listener)
|
fun start()
|
||||||
fun removeListener(listener: Listener)
|
fun dispose()
|
||||||
fun removeAllListeners()
|
|
||||||
|
|
||||||
interface Listener {
|
interface Listener {
|
||||||
|
fun onUpdated(snapshot: List<TimelineEvent>)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class Direction(val value: String) {
|
enum class Direction(val value: String) {
|
||||||
|
@ -27,6 +27,7 @@ import im.vector.matrix.android.api.session.room.model.RoomMember
|
|||||||
data class TimelineEvent(
|
data class TimelineEvent(
|
||||||
val root: Event,
|
val root: Event,
|
||||||
val localId: String,
|
val localId: String,
|
||||||
|
val displayIndex: Int,
|
||||||
val roomMember: RoomMember?
|
val roomMember: RoomMember?
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@ -16,22 +16,11 @@
|
|||||||
|
|
||||||
package im.vector.matrix.android.api.session.room.timeline
|
package im.vector.matrix.android.api.session.room.timeline
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This interface defines methods to interact with the timeline. It's implemented at the room level.
|
* This interface defines methods to interact with the timeline. It's implemented at the room level.
|
||||||
*/
|
*/
|
||||||
interface TimelineService {
|
interface TimelineService {
|
||||||
|
|
||||||
/**
|
|
||||||
* This is the main method of the service. It allows to listen for live [TimelineData].
|
|
||||||
* It's automatically refreshed as soon as timeline data gets updated, through sync or pagination.
|
|
||||||
*
|
|
||||||
* @param eventId: an optional eventId to start loading timeline around.
|
|
||||||
* @return the [LiveData] of [TimelineData]
|
|
||||||
*/
|
|
||||||
fun timeline(eventId: String? = null): LiveData<TimelineData>
|
|
||||||
|
|
||||||
fun createTimeline(eventId: String?): Timeline
|
fun createTimeline(eventId: String?): Timeline
|
||||||
|
|
||||||
}
|
}
|
@ -26,6 +26,7 @@ import java.util.concurrent.atomic.AtomicReference
|
|||||||
internal interface LiveEntityObserver {
|
internal interface LiveEntityObserver {
|
||||||
fun start()
|
fun start()
|
||||||
fun dispose()
|
fun dispose()
|
||||||
|
fun isStarted(): Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val monarchy: Monarchy)
|
internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val monarchy: Monarchy)
|
||||||
@ -56,6 +57,10 @@ internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val m
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun isStarted(): Boolean {
|
||||||
|
return isStarted.get()
|
||||||
|
}
|
||||||
|
|
||||||
protected open fun onChanged(realmResults: RealmResults<T>, changeSet: OrderedCollectionChangeSet) {
|
protected open fun onChanged(realmResults: RealmResults<T>, changeSet: OrderedCollectionChangeSet) {
|
||||||
val insertionIndexes = changeSet.insertions
|
val insertionIndexes = changeSet.insertions
|
||||||
val updateIndexes = changeSet.changes
|
val updateIndexes = changeSet.changes
|
||||||
|
@ -18,12 +18,13 @@ package im.vector.matrix.android.internal.database.model
|
|||||||
|
|
||||||
import io.realm.RealmObject
|
import io.realm.RealmObject
|
||||||
import io.realm.RealmResults
|
import io.realm.RealmResults
|
||||||
|
import io.realm.annotations.Index
|
||||||
import io.realm.annotations.LinkingObjects
|
import io.realm.annotations.LinkingObjects
|
||||||
import io.realm.annotations.PrimaryKey
|
import io.realm.annotations.PrimaryKey
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
internal open class EventEntity(@PrimaryKey var localId: String = UUID.randomUUID().toString(),
|
internal open class EventEntity(@PrimaryKey var localId: String = UUID.randomUUID().toString(),
|
||||||
var eventId: String = "",
|
@Index var eventId: String = "",
|
||||||
var roomId: String = "",
|
var roomId: String = "",
|
||||||
var type: String = "",
|
var type: String = "",
|
||||||
var content: String? = null,
|
var content: String? = null,
|
||||||
|
@ -28,7 +28,7 @@ import im.vector.matrix.android.internal.session.room.send.EventFactory
|
|||||||
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineService
|
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineService
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask
|
import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask
|
||||||
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.TimelineBoundaryCallback
|
import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFactory
|
||||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
import im.vector.matrix.android.internal.util.PagingRequestHelper
|
import im.vector.matrix.android.internal.util.PagingRequestHelper
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
@ -44,9 +44,9 @@ internal class RoomFactory(private val loadRoomMembersTask: LoadRoomMembersTask,
|
|||||||
|
|
||||||
fun instantiate(roomId: String): Room {
|
fun instantiate(roomId: String): Room {
|
||||||
val helper = PagingRequestHelper(Executors.newSingleThreadExecutor())
|
val helper = PagingRequestHelper(Executors.newSingleThreadExecutor())
|
||||||
val timelineBoundaryCallback = TimelineBoundaryCallback(roomId, taskExecutor, paginationTask, monarchy, helper)
|
|
||||||
val roomMemberExtractor = RoomMemberExtractor(monarchy, roomId)
|
val roomMemberExtractor = RoomMemberExtractor(monarchy, roomId)
|
||||||
val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, timelineBoundaryCallback, contextOfEventTask, roomMemberExtractor)
|
val timelineEventFactory = TimelineEventFactory(roomMemberExtractor)
|
||||||
|
val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask, helper)
|
||||||
val sendService = DefaultSendService(roomId, eventFactory, monarchy)
|
val sendService = DefaultSendService(roomId, eventFactory, monarchy)
|
||||||
val readService = DefaultReadService(roomId, monarchy, setReadMarkersTask, taskExecutor)
|
val readService = DefaultReadService(roomId, monarchy, setReadMarkersTask, taskExecutor)
|
||||||
return DefaultRoom(
|
return DefaultRoom(
|
||||||
|
@ -27,6 +27,7 @@ import im.vector.matrix.android.internal.session.room.RoomAPI
|
|||||||
import im.vector.matrix.android.internal.session.sync.SyncTokenStore
|
import im.vector.matrix.android.internal.session.sync.SyncTokenStore
|
||||||
import im.vector.matrix.android.internal.task.Task
|
import im.vector.matrix.android.internal.task.Task
|
||||||
import im.vector.matrix.android.internal.util.tryTransactionSync
|
import im.vector.matrix.android.internal.util.tryTransactionSync
|
||||||
|
import io.realm.kotlin.createObject
|
||||||
|
|
||||||
internal interface LoadRoomMembersTask : Task<LoadRoomMembersTask.Params, Boolean> {
|
internal interface LoadRoomMembersTask : Task<LoadRoomMembersTask.Params, Boolean> {
|
||||||
|
|
||||||
@ -60,7 +61,7 @@ internal class DefaultLoadRoomMembersTask(private val roomAPI: RoomAPI,
|
|||||||
.tryTransactionSync { realm ->
|
.tryTransactionSync { realm ->
|
||||||
// We ignore all the already known members
|
// We ignore all the already known members
|
||||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
||||||
?: throw IllegalStateException("You shouldn't use this method without a room")
|
?: realm.createObject(roomId)
|
||||||
|
|
||||||
val roomMembers = RoomMembers(realm, roomId).getLoaded()
|
val roomMembers = RoomMembers(realm, roomId).getLoaded()
|
||||||
val eventsToInsert = response.roomMemberEvents.filter { !roomMembers.containsKey(it.stateKey) }
|
val eventsToInsert = response.roomMemberEvents.filter { !roomMembers.containsKey(it.stateKey) }
|
||||||
@ -73,9 +74,9 @@ internal class DefaultLoadRoomMembersTask(private val roomAPI: RoomAPI,
|
|||||||
|
|
||||||
private fun areAllMembersAlreadyLoaded(roomId: String): Boolean {
|
private fun areAllMembersAlreadyLoaded(roomId: String): Boolean {
|
||||||
return monarchy
|
return monarchy
|
||||||
.fetchAllCopiedSync { RoomEntity.where(it, roomId) }
|
.fetchAllCopiedSync { RoomEntity.where(it, roomId) }
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
?.areAllMembersLoaded ?: false
|
?.areAllMembersLoaded ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -18,20 +18,26 @@
|
|||||||
|
|
||||||
package im.vector.matrix.android.internal.session.room.timeline
|
package im.vector.matrix.android.internal.session.room.timeline
|
||||||
|
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import androidx.annotation.UiThread
|
||||||
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
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.internal.database.RealmLiveEntityObserver
|
|
||||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
|
||||||
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
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor
|
|
||||||
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.util.PagingRequestHelper
|
||||||
import io.realm.OrderedCollectionChangeSet
|
import io.realm.OrderedCollectionChangeSet
|
||||||
|
import io.realm.OrderedRealmCollectionChangeListener
|
||||||
|
import io.realm.Realm
|
||||||
|
import io.realm.RealmConfiguration
|
||||||
|
import io.realm.RealmQuery
|
||||||
import io.realm.RealmResults
|
import io.realm.RealmResults
|
||||||
import io.realm.Sort
|
import io.realm.Sort
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
|
|
||||||
private const val INITIAL_LOAD_SIZE = 30
|
private const val INITIAL_LOAD_SIZE = 30
|
||||||
@ -39,103 +45,136 @@ private const val INITIAL_LOAD_SIZE = 30
|
|||||||
internal class DefaultTimeline(
|
internal class DefaultTimeline(
|
||||||
private val roomId: String,
|
private val roomId: String,
|
||||||
private val initialEventId: String? = null,
|
private val initialEventId: String? = null,
|
||||||
private val monarchy: Monarchy,
|
private val realmConfiguration: RealmConfiguration,
|
||||||
private val taskExecutor: TaskExecutor,
|
private val taskExecutor: TaskExecutor,
|
||||||
private val boundaryCallback: TimelineBoundaryCallback,
|
|
||||||
private val contextOfEventTask: GetContextOfEventTask,
|
private val contextOfEventTask: GetContextOfEventTask,
|
||||||
private val roomMemberExtractor: RoomMemberExtractor
|
private val timelineEventFactory: TimelineEventFactory,
|
||||||
|
private val paginationTask: PaginationTask,
|
||||||
|
private val helper: PagingRequestHelper
|
||||||
) : Timeline {
|
) : Timeline {
|
||||||
|
|
||||||
|
override var listener: Timeline.Listener? = null
|
||||||
|
|
||||||
|
private lateinit var realm: Realm
|
||||||
|
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 listeners = mutableListOf<Timeline.Listener>()
|
|
||||||
|
|
||||||
private val builtEvents = mutableListOf<TimelineEvent>()
|
private val changeListener = OrderedRealmCollectionChangeListener<RealmResults<EventEntity>> { _, changeSet ->
|
||||||
private lateinit var liveResults: RealmResults<EventEntity>
|
if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL) {
|
||||||
|
handleInitialLoad()
|
||||||
private val entityObserver = object : RealmLiveEntityObserver<EventEntity>(monarchy) {
|
} else {
|
||||||
|
|
||||||
override val query: Monarchy.Query<EventEntity>
|
|
||||||
get() = buildQuery(initialEventId)
|
|
||||||
|
|
||||||
override fun onChanged(realmResults: RealmResults<EventEntity>, changeSet: OrderedCollectionChangeSet) {
|
|
||||||
changeSet.insertionRanges.forEach {
|
changeSet.insertionRanges.forEach {
|
||||||
val (startIndex, direction) = if (it.startIndex == 0) {
|
val (startDisplayIndex, direction) = if (it.startIndex == 0) {
|
||||||
Pair(realmResults[it.length]!!.displayIndex, Timeline.Direction.FORWARDS)
|
Pair(liveEvents[it.length - 1]!!.displayIndex, Timeline.Direction.FORWARDS)
|
||||||
} else {
|
} else {
|
||||||
Pair(realmResults[it.startIndex]!!.displayIndex, Timeline.Direction.FORWARDS)
|
Pair(liveEvents[it.startIndex]!!.displayIndex, Timeline.Direction.BACKWARDS)
|
||||||
}
|
}
|
||||||
addFromLiveResults(startIndex, direction, it.length.toLong())
|
insertFromLiveResults(startDisplayIndex, direction, it.length.toLong())
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun processInitialResults(results: RealmResults<EventEntity>) {
|
|
||||||
// Results are ordered DESCENDING, so first items is the most recent
|
|
||||||
liveResults = results
|
|
||||||
val initialDisplayIndex = if (isLive) {
|
|
||||||
results.first()?.displayIndex
|
|
||||||
} else {
|
|
||||||
results.where().equalTo(EventEntityFields.EVENT_ID, initialEventId).findFirst()?.displayIndex
|
|
||||||
} ?: 0
|
|
||||||
prevDisplayIndex = initialDisplayIndex
|
|
||||||
nextDisplayIndex = initialDisplayIndex
|
|
||||||
val count = Math.min(INITIAL_LOAD_SIZE, results.size).toLong()
|
|
||||||
if (isLive) {
|
|
||||||
addFromLiveResults(initialDisplayIndex, Timeline.Direction.BACKWARDS, count)
|
|
||||||
} else {
|
|
||||||
val forwardCount = count / 2L
|
|
||||||
val backwardCount = count - forwardCount
|
|
||||||
addFromLiveResults(initialDisplayIndex, Timeline.Direction.BACKWARDS, backwardCount)
|
|
||||||
addFromLiveResults(initialDisplayIndex, Timeline.Direction.BACKWARDS, forwardCount)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UiThread
|
||||||
override fun paginate(direction: Timeline.Direction, count: Int) {
|
override fun paginate(direction: Timeline.Direction, count: Int) {
|
||||||
monarchy.postToMonarchyThread {
|
if (direction == Timeline.Direction.FORWARDS && isLive) {
|
||||||
val startDisplayIndex = if (direction == Timeline.Direction.BACKWARDS) prevDisplayIndex else nextDisplayIndex
|
return
|
||||||
val shouldHitNetwork = addFromLiveResults(startDisplayIndex, direction, count.toLong()).not()
|
}
|
||||||
if (shouldHitNetwork) {
|
val startDisplayIndex = if (direction == Timeline.Direction.BACKWARDS) prevDisplayIndex else nextDisplayIndex
|
||||||
if (direction == Timeline.Direction.BACKWARDS) {
|
val hasBuiltCountItems = insertFromLiveResults(startDisplayIndex, direction, count.toLong())
|
||||||
val itemAtEnd = builtEvents.last()
|
if (hasBuiltCountItems.not()) {
|
||||||
boundaryCallback.onItemAtEndLoaded(itemAtEnd)
|
val token = getToken(direction) ?: return
|
||||||
} else {
|
helper.runIfNotRunning(direction.toRequestType()) {
|
||||||
val itemAtFront = builtEvents.first()
|
executePaginationTask(it, token, direction.toPaginationDirection(), 30)
|
||||||
boundaryCallback.onItemAtFrontLoaded(itemAtFront)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addListener(listener: Timeline.Listener) {
|
@UiThread
|
||||||
if (listeners.isEmpty()) {
|
override fun start() {
|
||||||
entityObserver.start()
|
realm = Realm.getInstance(realmConfiguration)
|
||||||
}
|
liveEvents = buildQuery(initialEventId).findAllAsync()
|
||||||
listeners.add(listener)
|
liveEvents.addChangeListener(changeListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun removeListener(listener: Timeline.Listener) {
|
@UiThread
|
||||||
listeners.remove(listener)
|
override fun dispose() {
|
||||||
if (listeners.isEmpty()) {
|
liveEvents.removeAllChangeListeners()
|
||||||
entityObserver.dispose()
|
realm.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun snapshot(): List<TimelineEvent> = synchronized(builtEvents) {
|
||||||
|
return builtEvents.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun size(): Int = synchronized(builtEvents) {
|
||||||
|
return builtEvents.size
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleInitialLoad() = synchronized(builtEvents) {
|
||||||
|
val initialDisplayIndex = if (isLive) {
|
||||||
|
liveEvents.firstOrNull()?.displayIndex
|
||||||
|
} else {
|
||||||
|
liveEvents.where().equalTo(EventEntityFields.EVENT_ID, initialEventId).findFirst()?.displayIndex
|
||||||
|
} ?: 0
|
||||||
|
prevDisplayIndex = initialDisplayIndex
|
||||||
|
nextDisplayIndex = initialDisplayIndex
|
||||||
|
val count = Math.min(INITIAL_LOAD_SIZE, liveEvents.size).toLong()
|
||||||
|
if (count == 0L) {
|
||||||
|
return@synchronized
|
||||||
|
}
|
||||||
|
if (isLive) {
|
||||||
|
insertFromLiveResults(initialDisplayIndex, Timeline.Direction.BACKWARDS, count)
|
||||||
|
} else {
|
||||||
|
val forwardCount = count / 2L
|
||||||
|
val backwardCount = count - forwardCount
|
||||||
|
insertFromLiveResults(initialDisplayIndex, Timeline.Direction.BACKWARDS, backwardCount)
|
||||||
|
insertFromLiveResults(initialDisplayIndex, Timeline.Direction.BACKWARDS, forwardCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun removeAllListeners() {
|
private fun executePaginationTask(requestCallback: PagingRequestHelper.Request.Callback,
|
||||||
listeners.clear()
|
from: String,
|
||||||
if (listeners.isEmpty()) {
|
direction: PaginationDirection,
|
||||||
entityObserver.dispose()
|
limit: Int) {
|
||||||
}
|
|
||||||
|
val params = PaginationTask.Params(roomId = roomId,
|
||||||
|
from = from,
|
||||||
|
direction = direction,
|
||||||
|
limit = limit)
|
||||||
|
|
||||||
|
paginationTask.configureWith(params)
|
||||||
|
.enableRetry()
|
||||||
|
.dispatchTo(object : MatrixCallback<Boolean> {
|
||||||
|
override fun onSuccess(data: Boolean) {
|
||||||
|
requestCallback.recordSuccess()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(failure: Throwable) {
|
||||||
|
requestCallback.recordFailure(failure)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.executeBy(taskExecutor)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getToken(direction: Timeline.Direction): String? {
|
||||||
|
val chunkEntity = liveEvents.firstOrNull()?.chunk?.firstOrNull() ?: return null
|
||||||
|
return if (direction == Timeline.Direction.BACKWARDS) chunkEntity.prevToken else chunkEntity.nextToken
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* This has to be called on MonarchyThread as it access realm live results
|
||||||
* @return true if count items has been added
|
* @return true if count items has been added
|
||||||
*/
|
*/
|
||||||
private fun addFromLiveResults(startDisplayIndex: Int,
|
private fun insertFromLiveResults(startDisplayIndex: Int,
|
||||||
direction: Timeline.Direction,
|
direction: Timeline.Direction,
|
||||||
count: Long): Boolean {
|
count: Long): Boolean = synchronized(builtEvents) {
|
||||||
|
if (count < 1) {
|
||||||
|
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 false
|
||||||
@ -147,18 +186,18 @@ internal class DefaultTimeline(
|
|||||||
nextDisplayIndex = offsetIndex + 1
|
nextDisplayIndex = offsetIndex + 1
|
||||||
}
|
}
|
||||||
offsetResults.forEach { eventEntity ->
|
offsetResults.forEach { eventEntity ->
|
||||||
val roomMember = roomMemberExtractor.extractFrom(eventEntity)
|
val timelineEvent = timelineEventFactory.create(eventEntity)
|
||||||
val timelineEvent = TimelineEvent(eventEntity.asDomain(), eventEntity.localId, roomMember)
|
|
||||||
val position = if (direction == Timeline.Direction.FORWARDS) 0 else builtEvents.size
|
val position = if (direction == Timeline.Direction.FORWARDS) 0 else builtEvents.size
|
||||||
builtEvents.add(position, timelineEvent)
|
builtEvents.add(position, timelineEvent)
|
||||||
}
|
}
|
||||||
|
listener?.onUpdated(snapshot())
|
||||||
return offsetResults.size.toLong() == count
|
return offsetResults.size.toLong() == count
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getOffsetResults(startDisplayIndex: Int,
|
private fun getOffsetResults(startDisplayIndex: Int,
|
||||||
direction: Timeline.Direction,
|
direction: Timeline.Direction,
|
||||||
count: Long): RealmResults<EventEntity> {
|
count: Long): RealmResults<EventEntity> {
|
||||||
val offsetQuery = liveResults.where()
|
val offsetQuery = liveEvents.where()
|
||||||
if (direction == Timeline.Direction.BACKWARDS) {
|
if (direction == Timeline.Direction.BACKWARDS) {
|
||||||
offsetQuery
|
offsetQuery
|
||||||
.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
|
.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
|
||||||
@ -171,19 +210,27 @@ internal class DefaultTimeline(
|
|||||||
return offsetQuery.limit(count).findAll()
|
return offsetQuery.limit(count).findAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildQuery(eventId: String?): Monarchy.Query<EventEntity> {
|
private fun buildQuery(eventId: String?): RealmQuery<EventEntity> {
|
||||||
return Monarchy.Query<EventEntity> { realm ->
|
val query = if (eventId == null) {
|
||||||
val query = if (eventId == 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}", 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(eventId))
|
|
||||||
}
|
|
||||||
query.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
|
|
||||||
}
|
}
|
||||||
|
query.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
|
||||||
|
return query
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Timeline.Direction.toRequestType(): PagingRequestHelper.RequestType {
|
||||||
|
return if (this == Timeline.Direction.BACKWARDS) PagingRequestHelper.RequestType.BEFORE else PagingRequestHelper.RequestType.AFTER
|
||||||
|
}
|
||||||
|
|
||||||
|
//Todo : remove that
|
||||||
|
private fun Timeline.Direction.toPaginationDirection(): PaginationDirection {
|
||||||
|
return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS
|
||||||
|
}
|
||||||
}
|
}
|
@ -16,20 +16,17 @@
|
|||||||
|
|
||||||
package im.vector.matrix.android.internal.session.room.timeline
|
package im.vector.matrix.android.internal.session.room.timeline
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.paging.LivePagedListBuilder
|
|
||||||
import androidx.paging.PagedList
|
import androidx.paging.PagedList
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.api.session.room.timeline.*
|
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
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.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
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor
|
|
||||||
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.LiveDataUtils
|
|
||||||
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.Realm
|
||||||
@ -43,42 +40,16 @@ private const val EVENT_NOT_FOUND_INDEX = -1
|
|||||||
internal class DefaultTimelineService(private val roomId: String,
|
internal class DefaultTimelineService(private val roomId: String,
|
||||||
private val monarchy: Monarchy,
|
private val monarchy: Monarchy,
|
||||||
private val taskExecutor: TaskExecutor,
|
private val taskExecutor: TaskExecutor,
|
||||||
private val boundaryCallback: TimelineBoundaryCallback,
|
|
||||||
private val contextOfEventTask: GetContextOfEventTask,
|
private val contextOfEventTask: GetContextOfEventTask,
|
||||||
private val roomMemberExtractor: RoomMemberExtractor
|
private val timelineEventFactory: TimelineEventFactory,
|
||||||
|
private val paginationTask: PaginationTask,
|
||||||
|
private val helper: PagingRequestHelper
|
||||||
) : TimelineService {
|
) : TimelineService {
|
||||||
|
|
||||||
private val eventInterceptors = ArrayList<TimelineEventInterceptor>()
|
private val eventInterceptors = ArrayList<TimelineEventInterceptor>()
|
||||||
|
|
||||||
override fun timeline(eventId: String?): LiveData<TimelineData> {
|
|
||||||
clearUnlinkedEvents()
|
|
||||||
val initialLoadKey = getInitialLoadKey(eventId)
|
|
||||||
val realmDataSourceFactory = monarchy.createDataSourceFactory {
|
|
||||||
buildDataSourceFactoryQuery(it, eventId)
|
|
||||||
}
|
|
||||||
val domainSourceFactory = realmDataSourceFactory
|
|
||||||
.map { eventEntity ->
|
|
||||||
val roomMember = roomMemberExtractor.extractFrom(eventEntity)
|
|
||||||
TimelineEvent(eventEntity.asDomain(), eventEntity.localId, roomMember)
|
|
||||||
}
|
|
||||||
|
|
||||||
val pagedListConfig = buildPagedListConfig()
|
|
||||||
|
|
||||||
val livePagedListBuilder = LivePagedListBuilder(domainSourceFactory, pagedListConfig)
|
|
||||||
.setBoundaryCallback(boundaryCallback)
|
|
||||||
.setInitialLoadKey(initialLoadKey)
|
|
||||||
|
|
||||||
val eventsLiveData = monarchy.findAllPagedWithChanges(realmDataSourceFactory, livePagedListBuilder)
|
|
||||||
|
|
||||||
return LiveDataUtils.combine(eventsLiveData, boundaryCallback.status) { events, status ->
|
|
||||||
val isLoadingForward = status.before == PagingRequestHelper.Status.RUNNING
|
|
||||||
val isLoadingBackward = status.after == PagingRequestHelper.Status.RUNNING
|
|
||||||
TimelineData(events, isLoadingForward, isLoadingBackward)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createTimeline(eventId: String?): Timeline {
|
override fun createTimeline(eventId: String?): Timeline {
|
||||||
return DefaultTimeline(roomId, eventId, monarchy, taskExecutor, boundaryCallback, contextOfEventTask, roomMemberExtractor)
|
return DefaultTimeline(roomId, eventId, monarchy.realmConfiguration, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask, helper)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PRIVATE FUNCTIONS ***************************************************************************
|
// PRIVATE FUNCTIONS ***************************************************************************
|
||||||
@ -124,7 +95,8 @@ internal class DefaultTimelineService(private val roomId: String,
|
|||||||
private fun indexOfEvent(eventId: String): Int {
|
private fun indexOfEvent(eventId: String): Int {
|
||||||
var displayIndex = EVENT_NOT_FOUND_INDEX
|
var displayIndex = EVENT_NOT_FOUND_INDEX
|
||||||
monarchy.doWithRealm {
|
monarchy.doWithRealm {
|
||||||
displayIndex = EventEntity.where(it, eventId = eventId).findFirst()?.displayIndex ?: EVENT_NOT_FOUND_INDEX
|
displayIndex = EventEntity.where(it, eventId = eventId).findFirst()?.displayIndex
|
||||||
|
?: EVENT_NOT_FOUND_INDEX
|
||||||
}
|
}
|
||||||
return displayIndex
|
return displayIndex
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* 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.internal.session.room.timeline
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
|
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||||
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
|
import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor
|
||||||
|
|
||||||
|
internal class TimelineEventFactory(private val roomMemberExtractor: RoomMemberExtractor) {
|
||||||
|
|
||||||
|
fun create(eventEntity: EventEntity): TimelineEvent {
|
||||||
|
val roomMember = roomMemberExtractor.extractFrom(eventEntity)
|
||||||
|
return TimelineEvent(
|
||||||
|
eventEntity.asDomain(),
|
||||||
|
eventEntity.localId,
|
||||||
|
eventEntity.displayIndex,
|
||||||
|
roomMember
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -18,7 +18,12 @@ 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.*
|
import im.vector.matrix.android.internal.database.helper.addAll
|
||||||
|
import im.vector.matrix.android.internal.database.helper.addOrUpdate
|
||||||
|
import im.vector.matrix.android.internal.database.helper.addStateEvents
|
||||||
|
import im.vector.matrix.android.internal.database.helper.deleteOnCascade
|
||||||
|
import im.vector.matrix.android.internal.database.helper.isUnlinked
|
||||||
|
import im.vector.matrix.android.internal.database.helper.merge
|
||||||
import im.vector.matrix.android.internal.database.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
|
||||||
@ -26,6 +31,7 @@ import im.vector.matrix.android.internal.database.query.find
|
|||||||
import im.vector.matrix.android.internal.database.query.findAllIncludingEvents
|
import im.vector.matrix.android.internal.database.query.findAllIncludingEvents
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
import im.vector.matrix.android.internal.util.tryTransactionSync
|
import im.vector.matrix.android.internal.util.tryTransactionSync
|
||||||
|
import io.realm.kotlin.createObject
|
||||||
|
|
||||||
|
|
||||||
internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {
|
internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {
|
||||||
@ -40,7 +46,7 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {
|
|||||||
return monarchy
|
return monarchy
|
||||||
.tryTransactionSync { realm ->
|
.tryTransactionSync { realm ->
|
||||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
||||||
?: throw IllegalStateException("You shouldn't use this method without a room")
|
?: realm.createObject(roomId)
|
||||||
|
|
||||||
val nextToken: String?
|
val nextToken: String?
|
||||||
val prevToken: String?
|
val prevToken: String?
|
||||||
@ -60,10 +66,10 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {
|
|||||||
|
|
||||||
var currentChunk = if (direction == PaginationDirection.FORWARDS) {
|
var currentChunk = if (direction == PaginationDirection.FORWARDS) {
|
||||||
prevChunk?.apply { this.nextToken = nextToken }
|
prevChunk?.apply { this.nextToken = nextToken }
|
||||||
?: ChunkEntity.create(realm, prevToken, nextToken)
|
?: ChunkEntity.create(realm, prevToken, nextToken)
|
||||||
} else {
|
} else {
|
||||||
nextChunk?.apply { this.prevToken = prevToken }
|
nextChunk?.apply { this.prevToken = prevToken }
|
||||||
?: ChunkEntity.create(realm, prevToken, nextToken)
|
?: ChunkEntity.create(realm, prevToken, nextToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
currentChunk.addAll(roomId, receivedChunk.events, direction, isUnlinked = currentChunk.isUnlinked())
|
currentChunk.addAll(roomId, receivedChunk.events, direction, isUnlinked = currentChunk.isUnlinked())
|
||||||
|
Loading…
Reference in New Issue
Block a user