Timeline : introduce timeline data class to allow listening for isLoadingForward and isLoadingBackward

This commit is contained in:
ganfra 2019-01-07 19:38:36 +01:00
parent f5d64a5707
commit 1269715b5c
16 changed files with 192 additions and 94 deletions

View File

@ -6,9 +6,9 @@ import android.support.v7.widget.LinearLayoutManager
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.RiotFragment
import im.vector.riotredesign.core.platform.ToolbarConfigurable
@ -19,7 +19,6 @@ import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_room_detail.*
import org.koin.android.ext.android.inject
import org.koin.core.parameter.parametersOf
import timber.log.Timber

@Parcelize
data class RoomDetailArgs(
@ -72,7 +71,7 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {

private fun setupRecyclerView() {
val layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true)
//scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager)
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager)
recyclerView.layoutManager = layoutManager
//timelineEventController.addModelBuildListener { it.dispatchTo(scrollOnNewMessageCallback) }
recyclerView.setHasFixedSize(true)
@ -82,16 +81,19 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
}

private fun renderState(state: RoomDetailViewState) {
Timber.v("Render state")
val timeline = state.asyncTimeline()
if (timeline != null) {
renderTimeline(timeline)
}
renderRoomSummary(state.asyncRoomSummary())
renderRoomSummary(state)
renderTimeline(state)
}

private fun renderRoomSummary(roomSummary: RoomSummary?) {
roomSummary?.let {
private fun renderTimeline(state: RoomDetailViewState) {
when (state.asyncTimelineData) {
is Success -> timelineEventController.update(state.asyncTimelineData())

}
}

private fun renderRoomSummary(state: RoomDetailViewState) {
state.asyncRoomSummary()?.let {
toolbarTitleView.text = it.displayName
AvatarRenderer.render(it, toolbarAvatarImageView)
if (it.topic.isNotEmpty()) {
@ -103,11 +105,6 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
}
}

private fun renderTimeline(timeline: Timeline?) {
//scrollOnNewMessageCallback.hasBeenUpdated.set(true)
timelineEventController.timeline = timeline
}

// TimelineEventController.Callback ************************************************************

override fun onUrlClicked(url: String) {

View File

@ -53,9 +53,10 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,

private fun observeTimeline() {
room.rx().timeline(eventId)
.execute { asyncTimeline ->
copy(asyncTimeline = asyncTimeline)
.execute { timelineData ->
copy(asyncTimelineData= timelineData)
}
}


}

View File

@ -1,19 +1,16 @@
package im.vector.riotredesign.features.home.room.detail

import android.arch.paging.PagedList
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.events.model.EnrichedEvent
import im.vector.matrix.android.api.session.room.model.RoomSummary

typealias Timeline = PagedList<EnrichedEvent>
import im.vector.matrix.android.api.session.room.timeline.TimelineData

data class RoomDetailViewState(
val roomId: String,
val eventId: String?,
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
val asyncTimeline: Async<Timeline> = Uninitialized
val asyncTimelineData: Async<TimelineData> = Uninitialized
) : MvRxState {

constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId)

View File

@ -5,9 +5,9 @@ import com.airbnb.epoxy.EpoxyAsyncUtil
import com.airbnb.epoxy.EpoxyController
import im.vector.matrix.android.api.session.events.model.EnrichedEvent
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.timeline.TimelineData
import im.vector.riotredesign.core.extensions.localDateTime
import im.vector.riotredesign.features.home.LoadingItemModel_
import im.vector.riotredesign.features.home.room.detail.Timeline

class TimelineEventController(private val roomId: String,
private val messageItemFactory: MessageItemFactory,
@ -36,28 +36,38 @@ class TimelineEventController(private val roomId: String,
}
}

private var snapshotList: List<EnrichedEvent>? = emptyList()
var timeline: Timeline? = null
set(value) {
field?.removeWeakCallback(pagedListCallback)
field = value
field?.addWeakCallback(null, pagedListCallback)
buildSnapshotList()
}

private var snapshotList: List<EnrichedEvent> = emptyList()
private var timelineData: TimelineData? = null
var callback: Callback? = null

override fun buildModels() {
buildModels(snapshotList)
fun update(timelineData: TimelineData?) {
timelineData?.events?.removeWeakCallback(pagedListCallback)
this.timelineData = timelineData
timelineData?.events?.addWeakCallback(null, pagedListCallback)
buildSnapshotList()
}

private fun buildModels(data: List<EnrichedEvent?>?) {
if (data.isNullOrEmpty()) {
override fun buildModels() {
buildModelsWith(
snapshotList,
timelineData?.isLoadingForward ?: false,
timelineData?.isLoadingBackward ?: false
)
}

private fun buildModelsWith(events: List<EnrichedEvent?>,
isLoadingForward: Boolean,
isLoadingBackward: Boolean) {
if (events.isEmpty()) {
return
}
for (index in 0 until data.size) {
val event = data[index] ?: continue
val nextEvent = if (index + 1 < data.size) data[index + 1] else null
LoadingItemModel_()
.id(roomId + "forward_loading_item")
.addIf(isLoadingForward, this)

for (index in 0 until events.size) {
val event = events[index] ?: continue
val nextEvent = if (index + 1 < events.size) events[index + 1] else null

val date = event.root.localDateTime()
val nextDate = nextEvent?.root?.localDateTime()
@ -65,10 +75,13 @@ class TimelineEventController(private val roomId: String,

val item = when (event.root.type) {
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, addDaySeparator, date, callback)
else -> textItemFactory.create(event)
else -> textItemFactory.create(event)
}

item
?.onBind { timeline?.loadAround(index) }
?.onBind {
timelineData?.events?.loadAround(index)
}
?.id(event.localId)
?.addTo(this)

@ -78,15 +91,14 @@ class TimelineEventController(private val roomId: String,
}
}

//It's a hack at the moment
val isLastEvent = data.last()?.root?.type == EventType.STATE_ROOM_CREATE
LoadingItemModel_()
.id(roomId + "backward_loading_item")
.addIf(!isLastEvent, this)
.addIf(isLoadingBackward, this)

}

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


View File

@ -1,9 +1,8 @@
package im.vector.matrix.rx

import android.arch.paging.PagedList
import im.vector.matrix.android.api.session.events.model.EnrichedEvent
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.timeline.TimelineData
import io.reactivex.Observable

class RxRoom(private val room: Room) {
@ -12,7 +11,7 @@ class RxRoom(private val room: Room) {
return room.roomSummary.asObservable()
}

fun timeline(eventId: String? = null): Observable<PagedList<EnrichedEvent>> {
fun timeline(eventId: String? = null): Observable<TimelineData> {
return room.timeline(eventId).asObservable()
}


View File

@ -3,9 +3,10 @@ package im.vector.matrix.android.api.session.room
import android.arch.lifecycle.LiveData
import im.vector.matrix.android.api.session.room.model.MyMembership
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.timeline.TimelineService
import im.vector.matrix.android.api.util.Cancelable

interface Room : TimelineHolder, SendService {
interface Room : TimelineService, SendService {

val roomId: String


View File

@ -1,11 +0,0 @@
package im.vector.matrix.android.api.session.room

import android.arch.lifecycle.LiveData
import android.arch.paging.PagedList
import im.vector.matrix.android.api.session.events.model.EnrichedEvent

interface TimelineHolder {

fun timeline(eventId: String? = null): LiveData<PagedList<EnrichedEvent>>

}

View File

@ -0,0 +1,10 @@
package im.vector.matrix.android.api.session.room.timeline

import android.arch.paging.PagedList
import im.vector.matrix.android.api.session.events.model.EnrichedEvent

data class TimelineData(
val events: PagedList<EnrichedEvent>,
val isLoadingForward: Boolean = false,
val isLoadingBackward: Boolean = false
)

View File

@ -0,0 +1,9 @@
package im.vector.matrix.android.api.session.room.timeline

import android.arch.lifecycle.LiveData

interface TimelineService {

fun timeline(eventId: String? = null): LiveData<TimelineData>

}

View File

@ -9,10 +9,11 @@ import im.vector.matrix.android.api.session.events.model.EnrichedEvent
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.SendService
import im.vector.matrix.android.api.session.room.TimelineHolder
import im.vector.matrix.android.api.session.room.timeline.TimelineService
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.MyMembership
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.timeline.TimelineData
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
@ -32,7 +33,7 @@ internal data class DefaultRoom(

private val loadRoomMembersTask by inject<LoadRoomMembersTask>()
private val monarchy by inject<Monarchy>()
private val timelineHolder by inject<TimelineHolder> { parametersOf(roomId) }
private val timelineService by inject<TimelineService> { parametersOf(roomId) }
private val sendService by inject<SendService> { parametersOf(roomId) }
private val taskExecutor by inject<TaskExecutor>()

@ -47,8 +48,8 @@ internal data class DefaultRoom(
}
}

override fun timeline(eventId: String?): LiveData<PagedList<EnrichedEvent>> {
return timelineHolder.timeline(eventId)
override fun timeline(eventId: String?): LiveData<TimelineData> {
return timelineService.timeline(eventId)
}

override fun loadRoomMembersIfNeeded(): Cancelable {

View File

@ -2,14 +2,20 @@ package im.vector.matrix.android.internal.session.room

import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.room.SendService
import im.vector.matrix.android.api.session.room.TimelineHolder
import im.vector.matrix.android.api.session.room.send.EventFactory
import im.vector.matrix.android.api.session.room.timeline.TimelineService
import im.vector.matrix.android.internal.session.DefaultSession
import im.vector.matrix.android.internal.session.room.members.DefaultLoadRoomMembersTask
import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask
import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor
import im.vector.matrix.android.internal.session.room.send.DefaultSendService
import im.vector.matrix.android.internal.session.room.timeline.*
import im.vector.matrix.android.internal.session.room.timeline.DefaultGetContextOfEventTask
import im.vector.matrix.android.internal.session.room.timeline.DefaultPaginationTask
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.PaginationTask
import im.vector.matrix.android.internal.session.room.timeline.TimelineBoundaryCallback
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor
import im.vector.matrix.android.internal.util.PagingRequestHelper
import org.koin.dsl.module.module
import retrofit2.Retrofit
@ -50,7 +56,7 @@ class RoomModule {
val helper = PagingRequestHelper(Executors.newSingleThreadExecutor())
val timelineBoundaryCallback = TimelineBoundaryCallback(roomId, get(), get(), get(), helper)
val roomMemberExtractor = RoomMemberExtractor(get(), roomId)
DefaultTimelineHolder(roomId, get(), get(), timelineBoundaryCallback, get(), roomMemberExtractor) as TimelineHolder
DefaultTimelineService(roomId, get(), get(), timelineBoundaryCallback, get(), roomMemberExtractor) as TimelineService
}

factory { (roomId: String) ->

View File

@ -1,10 +1,9 @@
package im.vector.matrix.android.internal.session.room.timeline

import arrow.core.Try
import arrow.core.failure
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.network.executeRequest
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


@ -12,7 +11,7 @@ internal interface PaginationTask : Task<PaginationTask.Params, TokenChunkEvent>

data class Params(
val roomId: String,
val from: String?,
val from: String,
val direction: PaginationDirection,
val limit: Int
)
@ -24,9 +23,6 @@ internal class DefaultPaginationTask(private val roomAPI: RoomAPI,
) : PaginationTask {

override fun execute(params: PaginationTask.Params): Try<TokenChunkEvent> {
if (params.from == null) {
return RuntimeException("From token shouldn't be null").failure()
}
val filter = FilterUtil.createRoomEventFilter(true)?.toJSONString()
return executeRequest<PaginationResponse> {
apiCall = roomAPI.getRoomMessagesFrom(params.roomId, params.from, params.direction.value, params.limit, filter)
@ -36,4 +32,5 @@ internal class DefaultPaginationTask(private val roomAPI: RoomAPI,
.map { chunk }
}
}

}

View File

@ -6,7 +6,8 @@ import android.arch.paging.PagedList
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.interceptor.EnrichedEventInterceptor
import im.vector.matrix.android.api.session.events.model.EnrichedEvent
import im.vector.matrix.android.api.session.room.TimelineHolder
import im.vector.matrix.android.api.session.room.timeline.TimelineData
import im.vector.matrix.android.api.session.room.timeline.TimelineService
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.EventEntity
@ -15,23 +16,25 @@ 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.configureWith
import im.vector.matrix.android.internal.util.LiveDataUtils
import im.vector.matrix.android.internal.util.PagingRequestHelper
import im.vector.matrix.android.internal.util.tryTransactionAsync
import io.realm.Realm
import io.realm.RealmQuery

private const val PAGE_SIZE = 30

internal class DefaultTimelineHolder(private val roomId: String,
private val monarchy: Monarchy,
private val taskExecutor: TaskExecutor,
private val boundaryCallback: TimelineBoundaryCallback,
private val contextOfEventTask: GetContextOfEventTask,
private val roomMemberExtractor: RoomMemberExtractor
) : TimelineHolder {
internal class DefaultTimelineService(private val roomId: String,
private val monarchy: Monarchy,
private val taskExecutor: TaskExecutor,
private val boundaryCallback: TimelineBoundaryCallback,
private val contextOfEventTask: GetContextOfEventTask,
private val roomMemberExtractor: RoomMemberExtractor
) : TimelineService {

private val eventInterceptors = ArrayList<EnrichedEventInterceptor>()

override fun timeline(eventId: String?): LiveData<PagedList<EnrichedEvent>> {
override fun timeline(eventId: String?): LiveData<TimelineData> {
clearUnlinkedEvents()
if (eventId != null) {
fetchEventIfNeeded(eventId)
@ -51,9 +54,16 @@ internal class DefaultTimelineHolder(private val roomId: String,
.build()

val livePagedListBuilder = LivePagedListBuilder(domainSourceFactory, pagedListConfig).setBoundaryCallback(boundaryCallback)
return monarchy.findAllPagedWithChanges(realmDataSourceFactory, livePagedListBuilder)
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)
}
}


private fun clearUnlinkedEvents() {
monarchy.tryTransactionAsync { realm ->
val unlinkedEvents = EventEntity

View File

@ -1,5 +1,6 @@
package im.vector.matrix.android.internal.session.room.timeline

import android.arch.lifecycle.LiveData
import android.arch.paging.PagedList
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
@ -20,35 +21,60 @@ internal class TimelineBoundaryCallback(private val roomId: String,

var limit = 30

val status = object : LiveData<PagingRequestHelper.StatusReport>() {

init {
value = PagingRequestHelper.StatusReport.createDefault()
}

val listener = PagingRequestHelper.Listener { postValue(it) }

override fun onActive() {
helper.addListener(listener)
}

override fun onInactive() {
helper.removeListener(listener)
}
}

override fun onZeroItemsLoaded() {
// actually, it's not possible
}

override fun onItemAtEndLoaded(itemAtEnd: EnrichedEvent) {
val token = itemAtEnd.root.eventId?.let { getToken(it, PaginationDirection.BACKWARDS) }
?: return

helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER) {
runPaginationRequest(it, itemAtEnd, PaginationDirection.BACKWARDS)
runPaginationRequest(it, token, PaginationDirection.BACKWARDS)
}
}

override fun onItemAtFrontLoaded(itemAtFront: EnrichedEvent) {
val token = itemAtFront.root.eventId?.let { getToken(it, PaginationDirection.FORWARDS) }
?: return

helper.runIfNotRunning(PagingRequestHelper.RequestType.BEFORE) {
runPaginationRequest(it, itemAtFront, PaginationDirection.FORWARDS)
runPaginationRequest(it, token, PaginationDirection.FORWARDS)
}
}

private fun runPaginationRequest(requestCallback: PagingRequestHelper.Request.Callback,
item: EnrichedEvent,
direction: PaginationDirection) {
private fun getToken(eventId: String, direction: PaginationDirection): String? {
var token: String? = null
monarchy.doWithRealm { realm ->
if (item.root.eventId == null) {
return@doWithRealm
}
val chunkEntity = ChunkEntity.findAllIncludingEvents(realm, Collections.singletonList(item.root.eventId)).firstOrNull()
val chunkEntity = ChunkEntity.findAllIncludingEvents(realm, Collections.singletonList(eventId)).firstOrNull()
token = if (direction == PaginationDirection.FORWARDS) chunkEntity?.nextToken else chunkEntity?.prevToken
}
return token
}

private fun runPaginationRequest(requestCallback: PagingRequestHelper.Request.Callback,
from: String,
direction: PaginationDirection) {

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


View File

@ -0,0 +1,38 @@
package im.vector.matrix.android.internal.util

import android.arch.lifecycle.LiveData
import android.arch.lifecycle.MediatorLiveData

object LiveDataUtils {

fun <FIRST, SECOND, OUT> combine(firstSource: LiveData<FIRST>,
secondSource: LiveData<SECOND>,
mapper: (FIRST, SECOND) -> OUT): LiveData<OUT> {

return MediatorLiveData<OUT>().apply {
var firstValue: FIRST? = null
var secondValue: SECOND? = null

val valueDispatcher = {
firstValue?.let { safeFirst ->
secondValue?.let { safeSecond ->
val mappedValue = mapper(safeFirst, safeSecond)
postValue(mappedValue)
}
}
}


addSource(firstSource) {
firstValue = it
valueDispatcher()
}

addSource(secondSource) {
secondValue = it
valueDispatcher()
}
}
}

}

View File

@ -379,6 +379,11 @@ public class PagingRequestHelper {
@NonNull
private final Throwable[] mErrors;

public static StatusReport createDefault() {
final Throwable[] errors = {};
return new StatusReport(Status.SUCCESS, Status.SUCCESS, Status.SUCCESS, errors);
}

StatusReport(@NonNull Status initial, @NonNull Status before, @NonNull Status after,
@NonNull Throwable[] errors) {
this.initial = initial;