Continue to work on timeline/pagination. WIP

This commit is contained in:
ganfra 2018-10-19 20:34:54 +02:00
parent 6e3992e70e
commit 702abccb38
15 changed files with 122 additions and 72 deletions

View File

@ -9,4 +9,8 @@ fun AppCompatActivity.addFragment(fragment: Fragment, frameId: Int) {

fun AppCompatActivity.replaceFragment(fragment: Fragment, frameId: Int) {
supportFragmentManager.inTransaction { replace(frameId, fragment) }
}

fun AppCompatActivity.addFragmentToBackstack(fragment: Fragment, frameId: Int, tag: String? = null) {
supportFragmentManager.inTransaction { replace(frameId, fragment).addToBackStack(tag) }
}

View File

@ -16,4 +16,12 @@ fun Fragment.addChildFragment(fragment: Fragment, frameId: Int) {

fun Fragment.replaceChildFragment(fragment: Fragment, frameId: Int) {
childFragmentManager.inTransaction { replace(frameId, fragment) }
}

fun Fragment.addFragmentToBackstack(fragment: Fragment, frameId: Int, tag: String? = null) {
fragmentManager?.inTransaction { replace(frameId, fragment).addToBackStack(tag) }
}

fun Fragment.addChildFragmentToBackstack(fragment: Fragment, frameId: Int, tag: String? = null) {
childFragmentManager.inTransaction { replace(frameId, fragment).addToBackStack(tag) }
}

View File

@ -3,19 +3,21 @@ package im.vector.riotredesign.features.home
import android.arch.lifecycle.Observer
import android.arch.paging.PagedList
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.Room
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.RiotFragment
import im.vector.riotredesign.core.utils.FragmentArgumentDelegate
import kotlinx.android.synthetic.main.fragment_room_list.*
import org.koin.android.ext.android.inject

class RoomDetailFragment : RiotFragment(), RoomController.Callback {
class RoomDetailFragment : RiotFragment() {

companion object {

@ -29,11 +31,8 @@ class RoomDetailFragment : RiotFragment(), RoomController.Callback {
private val matrix by inject<Matrix>()
private val currentSession = matrix.currentSession!!
private var roomId by FragmentArgumentDelegate<String>()

private val timelineController = TimelineEventController()
private val room: Room? by lazy {
currentSession.getRoom(roomId)
}
private lateinit var room: Room

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_room_detail, container, false)
@ -41,19 +40,20 @@ class RoomDetailFragment : RiotFragment(), RoomController.Callback {

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
if (room == null) {
activity?.onBackPressed()
return
}
room?.liveTimeline()?.observe(this, Observer { renderEvents(it) })
setupRecyclerView()
room = currentSession.getRoom(roomId)!!
room.liveTimeline().observe(this, Observer { renderEvents(it) })
}

private fun renderEvents(events: PagedList<Event>?) {
timelineController.submitList(events)
}

override fun onRoomSelected(room: Room) {
Toast.makeText(context, "Room ${room.roomId} clicked", Toast.LENGTH_SHORT).show()
private fun setupRecyclerView() {
val linearLayoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
linearLayoutManager.stackFromEnd = true
epoxyRecyclerView.layoutManager = linearLayoutManager
epoxyRecyclerView.setController(timelineController)
}



View File

@ -2,13 +2,14 @@ package im.vector.riotredesign.features.home

import android.arch.lifecycle.Observer
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.session.room.Room
import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.replaceFragment
import im.vector.riotredesign.core.extensions.addFragmentToBackstack
import im.vector.riotredesign.core.platform.RiotFragment
import kotlinx.android.synthetic.main.fragment_room_list.*
import org.koin.android.ext.android.inject
@ -43,7 +44,7 @@ class RoomListFragment : RiotFragment(), RoomController.Callback {

override fun onRoomSelected(room: Room) {
val detailFragment = RoomDetailFragment.newInstance(room.roomId)
replaceFragment(detailFragment, R.id.homeFragmentContainer)
addFragmentToBackstack(detailFragment, R.id.homeFragmentContainer)
}



View File

@ -6,14 +6,15 @@ import com.airbnb.epoxy.paging.PagedListEpoxyController
import im.vector.matrix.android.api.session.events.model.Event

class TimelineEventController : PagedListEpoxyController<Event>(
modelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
modelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler(),
diffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
) {

override fun buildItemModel(currentPosition: Int, item: Event?): EpoxyModel<*> {
return if (item == null) {
LoadingItemModel_().id(-currentPosition)
} else {
TimelineEventItem(item.eventId ?: "$currentPosition").id(currentPosition)
TimelineEventItem(item.toString()).id(currentPosition)
}
}


View File

@ -1,10 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/titleView"
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="80dp"
android:padding="16dp"
android:textSize="14sp"
tools:text="Room name" />
android:textSize="14sp" />

View File

@ -1,5 +1,6 @@
package im.vector.matrix.android.internal.database.query

import im.vector.matrix.android.internal.database.DBConstants
import im.vector.matrix.android.internal.database.model.ChunkEntity
import io.realm.Realm
import io.realm.RealmQuery
@ -40,5 +41,7 @@ fun ChunkEntity.Companion.findLastFromRoom(realm: Realm, roomId: String): ChunkE
fun ChunkEntity.Companion.findAllIncludingEvents(realm: Realm, eventIds: List<String>): RealmResults<ChunkEntity> {
return realm.where(ChunkEntity::class.java)
.`in`("events.eventId", eventIds.toTypedArray())
.notEqualTo("prevToken", DBConstants.STATE_EVENTS_CHUNK_TOKEN)
.notEqualTo("nextToken", DBConstants.STATE_EVENTS_CHUNK_TOKEN)
.findAll()
}

View File

@ -11,11 +11,15 @@ fun EventEntity.Companion.where(realm: Realm, roomId: String): RealmQuery<EventE
.equalTo("chunk.room.roomId", roomId)
}

fun EventEntity.Companion.where(realm: Realm, chunk: ChunkEntity): RealmQuery<EventEntity> {
return realm.where(EventEntity::class.java)
.equalTo("chunk.prevToken", chunk.prevToken)
.and()
.equalTo("chunk.nextToken", chunk.nextToken)
fun EventEntity.Companion.where(realm: Realm, chunk: ChunkEntity?): RealmQuery<EventEntity> {
var query = realm.where(EventEntity::class.java)
if (chunk?.prevToken != null) {
query = query.equalTo("chunk.prevToken", chunk.prevToken)
}
if (chunk?.nextToken != null) {
query = query.equalTo("chunk.nextToken", chunk.nextToken)
}
return query
}

fun RealmResults<EventEntity>.getLast(type: String? = null): EventEntity? {
@ -23,5 +27,5 @@ fun RealmResults<EventEntity>.getLast(type: String? = null): EventEntity? {
if (type != null) {
query = query.equalTo("type", type)
}
return query.findAll().sort("age").last(null)
return query.findAll().last(null)
}

View File

@ -5,7 +5,9 @@ import im.vector.matrix.android.internal.network.parsing.UriMoshiAdapter

object MoshiProvider {

private val moshi: Moshi = Moshi.Builder().add(UriMoshiAdapter()).build()
private val moshi: Moshi = Moshi.Builder()
.add(UriMoshiAdapter())
.build()

fun providesMoshi(): Moshi {
return moshi

View File

@ -8,7 +8,6 @@ import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.internal.database.mapper.EventMapper
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.room.timeline.PaginationRequest
import im.vector.matrix.android.internal.session.room.timeline.TimelineBoundaryCallback
@ -26,15 +25,17 @@ data class DefaultRoom(

override fun liveTimeline(): LiveData<PagedList<Event>> {
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
val lastChunk = ChunkEntity.where(realm, roomId).findAll().last(null)
if (lastChunk == null) {
EventEntity.where(realm, roomId)
} else {
EventEntity.where(realm, lastChunk)
}
ChunkEntity.where(realm, roomId).findAll().last(null).let { it?.events }?.where()
}
val domainSourceFactory = realmDataSourceFactory.map { EventMapper.map(it) }
val livePagedListBuilder = LivePagedListBuilder(domainSourceFactory, 20).setBoundaryCallback(boundaryCallback)

val pagedListConfig = PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setPageSize(10)
.setPrefetchDistance(5)
.build()

val livePagedListBuilder = LivePagedListBuilder(domainSourceFactory, pagedListConfig).setBoundaryCallback(boundaryCallback)
return monarchy.findAllPagedWithChanges(realmDataSourceFactory, livePagedListBuilder)
}


View File

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

import im.vector.matrix.android.internal.network.NetworkConstants
import im.vector.matrix.android.internal.session.room.model.TokenChunkEvent
import kotlinx.coroutines.Deferred
import retrofit2.Response
@ -18,12 +19,13 @@ interface RoomAPI {
* @param limit the maximum number of messages to retrieve. Optional.
* @param filter A JSON RoomEventFilter to filter returned events with. Optional.
*/
@GET("rooms/{roomId}/messages")
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/messages")
fun getRoomMessagesFrom(@Path("roomId") roomId: String,
@Query("from") from: String,
@Query("dir") dir: String,
@Query("limit") limit: Int,
@Query("filter") filter: String?): Deferred<Response<TokenChunkEvent>>
@Query("filter") filter: String?
): Deferred<Response<TokenChunkEvent>>


}

View File

@ -0,0 +1,15 @@
package im.vector.matrix.android.internal.session.room.model

enum class PaginationDirection(val value: String) {
/**
* Forwards when the event is added to the end of the timeline.
* These events come from the /sync stream or from forwards pagination.
*/
FORWARDS("f"),

/**
* Backwards when the event is added to the start of the timeline.
* These events come from a back pagination.
*/
BACKWARDS("b")
}

View File

@ -6,8 +6,8 @@ import im.vector.matrix.android.api.session.events.model.Event

@JsonClass(generateAdapter = true)
data class TokenChunkEvent(
@Json(name = "start") val prevToken: String? = null,
@Json(name = "end") val nextToken: String? = null,
@Json(name = "chunks") val chunk: List<Event> = emptyList(),
@Json(name = "start") val nextToken: String? = null,
@Json(name = "end") val prevToken: String? = null,
@Json(name = "chunk") val chunk: List<Event> = emptyList(),
@Json(name = "state") val stateEvents: List<Event> = emptyList()
)

View File

@ -9,6 +9,7 @@ import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.database.mapper.asEntity
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.query.findAllIncludingEvents
import im.vector.matrix.android.internal.database.query.findWithNextToken
@ -16,6 +17,7 @@ import im.vector.matrix.android.internal.database.query.findWithPrevToken
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.session.room.model.PaginationDirection
import im.vector.matrix.android.internal.session.room.model.TokenChunkEvent
import im.vector.matrix.android.internal.util.CancelableCoroutine
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
@ -28,8 +30,8 @@ class PaginationRequest(private val roomAPI: RoomAPI,
private val coroutineDispatchers: MatrixCoroutineDispatchers) {

fun execute(roomId: String,
from: String? = null,
direction: String,
from: String?,
direction: PaginationDirection,
limit: Int = 10,
filter: String? = null,
callback: MatrixCallback<TokenChunkEvent>
@ -42,22 +44,24 @@ class PaginationRequest(private val roomAPI: RoomAPI,
}

private suspend fun execute(roomId: String,
from: String? = null,
direction: String,
from: String?,
direction: PaginationDirection,
limit: Int = 10,
filter: String? = null) = withContext(coroutineDispatchers.io) {
filter: String?) = withContext(coroutineDispatchers.io) {

if (from == null) {
return@withContext Either.left(Failure.Unknown(RuntimeException("From token can't be null")))
return@withContext Either.left(
Failure.Unknown(RuntimeException("From token shouldn't be null"))
)
}
executeRequest<TokenChunkEvent> {
apiCall = roomAPI.getRoomMessagesFrom(roomId, from, direction, limit, filter)
return@withContext executeRequest<TokenChunkEvent> {
apiCall = roomAPI.getRoomMessagesFrom(roomId, from, direction.value, limit, filter)
}.leftIfNull {
Failure.Unknown(RuntimeException("TokenChunkEvent shouldn't be null"))
}.flatMap {
}.flatMap { chunk ->
try {
insertInDb(it, roomId)
Either.right(it)
insertInDb(chunk, roomId)
Either.right(chunk)
} catch (exception: Exception) {
Either.Left(Failure.Unknown(exception))
}
@ -67,7 +71,7 @@ class PaginationRequest(private val roomAPI: RoomAPI,
private fun insertInDb(chunkEvent: TokenChunkEvent, roomId: String) {
monarchy.runTransactionSync { realm ->
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: return@runTransactionSync
?: return@runTransactionSync

val nextChunk = ChunkEntity.findWithPrevToken(realm, roomId, chunkEvent.nextToken)
val prevChunk = ChunkEntity.findWithNextToken(realm, roomId, chunkEvent.prevToken)
@ -75,38 +79,42 @@ class PaginationRequest(private val roomAPI: RoomAPI,
val mergedEvents = chunkEvent.chunk + chunkEvent.stateEvents
val mergedEventIds = mergedEvents.filter { it.eventId != null }.map { it.eventId!! }
val chunksOverlapped = ChunkEntity.findAllIncludingEvents(realm, mergedEventIds)
val hasOverlapped = chunksOverlapped.isNotEmpty()

val currentChunk: ChunkEntity
if (nextChunk != null) {
currentChunk = nextChunk
val currentChunk = if (nextChunk != null) {
nextChunk
} else {
currentChunk = ChunkEntity()
ChunkEntity()
}


val eventsToAdd = ArrayList<EventEntity>()

currentChunk.prevToken = chunkEvent.prevToken
mergedEvents.forEach { event ->
val eventEntity = event.asEntity().let {
realm.copyToRealmOrUpdate(it)
}
if (!currentChunk.events.contains(eventEntity)) {
currentChunk.events.add(eventEntity)
eventsToAdd.add(0, eventEntity)
}
}

if (prevChunk != null) {
currentChunk.events.addAll(prevChunk.events)
eventsToAdd.addAll(0, prevChunk.events)
roomEntity.chunks.remove(prevChunk)

} else if (chunksOverlapped.isNotEmpty()) {
} else if (hasOverlapped) {
chunksOverlapped.forEach { chunk ->
chunk.events.forEach { event ->
if (!currentChunk.events.contains(event)) {
currentChunk.events.add(event)
eventsToAdd.add(0, event)
}
}
roomEntity.chunks.remove(chunk)
}
}

currentChunk.events.addAll(0, eventsToAdd)
if (!roomEntity.chunks.contains(currentChunk)) {
roomEntity.chunks.add(currentChunk)
}

View File

@ -7,6 +7,7 @@ import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.query.findAllIncludingEvents
import im.vector.matrix.android.internal.session.room.model.PaginationDirection
import im.vector.matrix.android.internal.session.room.model.TokenChunkEvent
import im.vector.matrix.android.internal.util.PagingRequestHelper
import java.util.*
@ -25,19 +26,20 @@ class TimelineBoundaryCallback(private val paginationRequest: PaginationRequest,
}

override fun onItemAtEndLoaded(itemAtEnd: Event) {
helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER) {
monarchy.doWithRealm { realm ->
if (itemAtEnd.eventId == null) {
return@doWithRealm
}
val chunkEntity = ChunkEntity.findAllIncludingEvents(realm, Collections.singletonList(itemAtEnd.eventId)).firstOrNull()
paginationRequest.execute(roomId, chunkEntity?.prevToken, "forward", callback = createCallback(it))
}
}
//Todo handle forward pagination
}

override fun onItemAtFrontLoaded(itemAtFront: Event) {
//Todo handle forward pagination
helper.runIfNotRunning(PagingRequestHelper.RequestType.BEFORE) {
monarchy.doWithRealm { realm ->
if (itemAtFront.eventId == null) {
return@doWithRealm
}
val chunkEntity = ChunkEntity.findAllIncludingEvents(realm, Collections.singletonList(itemAtFront.eventId)).firstOrNull()
paginationRequest.execute(roomId, chunkEntity?.prevToken, PaginationDirection.BACKWARDS, callback = createCallback(it))
}
}

}

private fun createCallback(pagingRequestCallback: PagingRequestHelper.Request.Callback) = object : MatrixCallback<TokenChunkEvent> {