From 9f79a5132df554d505fbad5c226dc967f1917149 Mon Sep 17 00:00:00 2001
From: ganfra <francois.ganard@gmail.com>
Date: Thu, 29 Nov 2018 11:52:36 +0100
Subject: [PATCH] Pagination/Permalink : extract persistence logic in a
 dedicated class

---
 .../android/internal/session/room/RoomAPI.kt  | 11 +--
 .../internal/session/room/RoomModule.kt       |  9 ++-
 .../room/timeline/EventContextResponse.kt     | 13 +++-
 .../room/timeline/GetContextOfEventRequest.kt | 47 +----------
 .../room/timeline/PaginationRequest.kt        | 78 ++-----------------
 .../room/timeline/PaginationResponse.kt       | 13 ++++
 .../session/room/timeline/TokenChunkEvent.kt  | 15 ++--
 .../room/timeline/TokenChunkEventPersistor.kt | 73 +++++++++++++++++
 8 files changed, 116 insertions(+), 143 deletions(-)
 create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationResponse.kt
 create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt

diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt
index 28c9776fb0..7c17f49f4b 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt
@@ -2,18 +2,13 @@ package im.vector.matrix.android.internal.session.room
 
 import im.vector.matrix.android.api.session.events.model.Content
 import im.vector.matrix.android.api.session.events.model.Event
-import im.vector.matrix.android.api.session.room.model.MessageContent
 import im.vector.matrix.android.internal.network.NetworkConstants
 import im.vector.matrix.android.internal.session.room.members.RoomMembersResponse
 import im.vector.matrix.android.internal.session.room.send.SendResponse
 import im.vector.matrix.android.internal.session.room.timeline.EventContextResponse
-import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEvent
+import im.vector.matrix.android.internal.session.room.timeline.PaginationResponse
 import retrofit2.Call
-import retrofit2.http.Body
-import retrofit2.http.GET
-import retrofit2.http.PUT
-import retrofit2.http.Path
-import retrofit2.http.Query
+import retrofit2.http.*
 
 internal interface RoomAPI {
 
@@ -32,7 +27,7 @@ internal interface RoomAPI {
                             @Query("dir") dir: String,
                             @Query("limit") limit: Int,
                             @Query("filter") filter: String?
-    ): Call<TokenChunkEvent>
+    ): Call<PaginationResponse>
 
 
     /**
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt
index 7bed72864c..9a7e897eaf 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt
@@ -7,10 +7,7 @@ import im.vector.matrix.android.api.session.room.send.EventFactory
 import im.vector.matrix.android.internal.session.DefaultSession
 import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersRequest
 import im.vector.matrix.android.internal.session.room.send.DefaultSendService
-import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineHolder
-import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventRequest
-import im.vector.matrix.android.internal.session.room.timeline.PaginationRequest
-import im.vector.matrix.android.internal.session.room.timeline.TimelineBoundaryCallback
+import im.vector.matrix.android.internal.session.room.timeline.*
 import im.vector.matrix.android.internal.util.PagingRequestHelper
 import org.koin.dsl.context.ModuleDefinition
 import org.koin.dsl.module.Module
@@ -32,6 +29,10 @@ class RoomModule : Module {
             LoadRoomMembersRequest(get(), get(), get())
         }
 
+        scope(DefaultSession.SCOPE) {
+            TokenChunkEventPersistor(get())
+        }
+
         scope(DefaultSession.SCOPE) {
             PaginationRequest(get(), get(), get())
         }
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt
index 76ea2a8ae8..0579b44cd6 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt
@@ -7,9 +7,14 @@ import im.vector.matrix.android.api.session.events.model.Event
 @JsonClass(generateAdapter = true)
 data class EventContextResponse(
         @Json(name = "event") val event: Event,
-        @Json(name = "start") val prevToken: String? = null,
+        @Json(name = "start") override val prevToken: String? = null,
         @Json(name = "events_before") val eventsBefore: List<Event> = emptyList(),
         @Json(name = "events_after") val eventsAfter: List<Event> = emptyList(),
-        @Json(name = "end") val nextToken: String? = null,
-        @Json(name = "state") val stateEvents: List<Event> = emptyList()
-)
+        @Json(name = "end") override val nextToken: String? = null,
+        @Json(name = "state") override val stateEvents: List<Event> = emptyList()
+) : TokenChunkEvent {
+
+    override val events: List<Event>
+        get() = listOf(event)
+
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/GetContextOfEventRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/GetContextOfEventRequest.kt
index 9ffad74dfa..868d695305 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/GetContextOfEventRequest.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/GetContextOfEventRequest.kt
@@ -1,31 +1,18 @@
 package im.vector.matrix.android.internal.session.room.timeline
 
-import arrow.core.Try
-import com.zhuinden.monarchy.Monarchy
 import im.vector.matrix.android.api.MatrixCallback
 import im.vector.matrix.android.api.util.Cancelable
-import im.vector.matrix.android.internal.database.helper.addOrUpdate
-import im.vector.matrix.android.internal.database.helper.add
-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.merge
-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.query.find
-import im.vector.matrix.android.internal.database.query.where
 import im.vector.matrix.android.internal.legacy.util.FilterUtil
 import im.vector.matrix.android.internal.network.executeRequest
 import im.vector.matrix.android.internal.session.room.RoomAPI
 import im.vector.matrix.android.internal.util.CancelableCoroutine
 import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
-import im.vector.matrix.android.internal.util.tryTransactionSync
-import io.realm.kotlin.createObject
 import kotlinx.coroutines.GlobalScope
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 
 internal class GetContextOfEventRequest(private val roomAPI: RoomAPI,
-                                        private val monarchy: Monarchy,
+                                        private val tokenChunkEventPersistor: TokenChunkEventPersistor,
                                         private val coroutineDispatchers: MatrixCoroutineDispatchers
 ) {
 
@@ -48,39 +35,9 @@ internal class GetContextOfEventRequest(private val roomAPI: RoomAPI,
         executeRequest<EventContextResponse> {
             apiCall = roomAPI.getContextOfEvent(roomId, eventId, 0, filter)
         }.flatMap { response ->
-            insertInDb(response, roomId)
+            tokenChunkEventPersistor.insertInDb(response, roomId, PaginationDirection.BACKWARDS).map { response }
         }
     }
 
-    private fun insertInDb(response: EventContextResponse, roomId: String): Try<EventContextResponse> {
-        return monarchy
-                .tryTransactionSync { realm ->
-                    val roomEntity = RoomEntity.where(realm, roomId).findFirst()
-                            ?: throw IllegalStateException("You shouldn't use this method without a room")
-
-                    val currentChunk = realm.createObject<ChunkEntity>().apply {
-                        prevToken = response.prevToken
-                        nextToken = response.nextToken
-                    }
-
-                    currentChunk.add(response.event, PaginationDirection.FORWARDS, isUnlinked = true)
-                    // Now, handles chunk merge
-                    val prevChunk = ChunkEntity.find(realm, roomId, nextToken = response.prevToken)
-                    val nextChunk = ChunkEntity.find(realm, roomId, prevToken = response.nextToken)
-
-                    if (prevChunk != null) {
-                        currentChunk.merge(prevChunk, PaginationDirection.BACKWARDS)
-                        roomEntity.deleteOnCascade(prevChunk)
-                    }
-                    if (nextChunk != null) {
-                        currentChunk.merge(nextChunk, PaginationDirection.FORWARDS)
-                        roomEntity.deleteOnCascade(nextChunk)
-                    }
-                    roomEntity.addOrUpdate(currentChunk)
-                    roomEntity.addStateEvents(response.stateEvents, stateIndex = Int.MIN_VALUE, isUnlinked = true)
-                }
-                .map { response }
-    }
-
 
 }
\ No newline at end of file
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationRequest.kt
index 795ce5c29e..4f603b9b08 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationRequest.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationRequest.kt
@@ -1,34 +1,19 @@
 package im.vector.matrix.android.internal.session.room.timeline
 
-import arrow.core.Try
 import arrow.core.failure
-import com.zhuinden.monarchy.Monarchy
 import im.vector.matrix.android.api.MatrixCallback
 import im.vector.matrix.android.api.util.Cancelable
-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.RoomEntity
-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.where
 import im.vector.matrix.android.internal.legacy.util.FilterUtil
 import im.vector.matrix.android.internal.network.executeRequest
 import im.vector.matrix.android.internal.session.room.RoomAPI
 import im.vector.matrix.android.internal.util.CancelableCoroutine
 import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
-import im.vector.matrix.android.internal.util.tryTransactionSync
-import io.realm.kotlin.createObject
 import kotlinx.coroutines.GlobalScope
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 
 internal class PaginationRequest(private val roomAPI: RoomAPI,
-                                 private val monarchy: Monarchy,
+                                 private val tokenChunkEventPersistor: TokenChunkEventPersistor,
                                  private val coroutineDispatchers: MatrixCoroutineDispatchers
 ) {
 
@@ -55,66 +40,13 @@ internal class PaginationRequest(private val roomAPI: RoomAPI,
         if (from == null) {
             return@withContext RuntimeException("From token shouldn't be null").failure<TokenChunkEvent>()
         }
-        executeRequest<TokenChunkEvent> {
+        executeRequest<PaginationResponse> {
             apiCall = roomAPI.getRoomMessagesFrom(roomId, from, direction.value, limit, filter)
         }.flatMap { chunk ->
-            insertInDb(chunk, roomId, direction)
+            tokenChunkEventPersistor
+                    .insertInDb(chunk, roomId, direction)
+                    .map { chunk }
         }
     }
 
-    private fun insertInDb(receivedChunk: TokenChunkEvent,
-                           roomId: String,
-                           direction: PaginationDirection): Try<TokenChunkEvent> {
-        return monarchy
-                .tryTransactionSync { realm ->
-                    val roomEntity = RoomEntity.where(realm, roomId).findFirst()
-                                     ?: throw IllegalStateException("You shouldn't use this method without a room")
-
-                    // We create a new chunk with prev and next token as a base
-                    // In case of permalink, we may not encounter other chunks, so it can be added
-                    val newChunk = realm.createObject<ChunkEntity>().apply {
-                        prevToken = receivedChunk.prevToken
-                        nextToken = receivedChunk.nextToken
-                    }
-                    newChunk.addAll(receivedChunk.events, direction, isUnlinked = true)
-
-                    // The current chunk is the one we will keep all along the merge process.
-                    var currentChunk = newChunk
-                    val prevChunk = ChunkEntity.find(realm, roomId, nextToken = receivedChunk.prevToken)
-                    val nextChunk = ChunkEntity.find(realm, roomId, prevToken = receivedChunk.nextToken)
-
-                    // We always merge the bottom chunk into top chunk, so we are always merging backwards
-                    if (prevChunk != null) {
-                        newChunk.merge(prevChunk, PaginationDirection.BACKWARDS)
-                        roomEntity.deleteOnCascade(prevChunk)
-                    }
-                    if (nextChunk != null) {
-                        nextChunk.merge(newChunk, PaginationDirection.BACKWARDS)
-                        newChunk.deleteOnCascade()
-                        currentChunk = nextChunk
-                    }
-                    val newEventIds = receivedChunk.events.mapNotNull { it.eventId }
-                    ChunkEntity
-                            .findAllIncludingEvents(realm, newEventIds)
-                            .filter { it != currentChunk }
-                            .forEach { overlapped ->
-                                if (direction == PaginationDirection.BACKWARDS) {
-                                    currentChunk.merge(overlapped, PaginationDirection.BACKWARDS)
-                                    roomEntity.deleteOnCascade(overlapped)
-                                } else {
-                                    overlapped.merge(currentChunk, PaginationDirection.BACKWARDS)
-                                    currentChunk = overlapped
-                                }
-                            }
-
-                    roomEntity.addOrUpdate(currentChunk)
-
-                    // TODO : there is an issue with the pagination sending unwanted room member events
-                    val isUnlinked = currentChunk.isUnlinked()
-                    roomEntity.addStateEvents(receivedChunk.stateEvents, isUnlinked = isUnlinked)
-                }
-                .map { receivedChunk }
-    }
-
-
 }
\ No newline at end of file
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationResponse.kt
new file mode 100644
index 0000000000..edf56c28bd
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationResponse.kt
@@ -0,0 +1,13 @@
+package im.vector.matrix.android.internal.session.room.timeline
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+import im.vector.matrix.android.api.session.events.model.Event
+
+@JsonClass(generateAdapter = true)
+internal data class PaginationResponse(
+        @Json(name = "start") override val nextToken: String? = null,
+        @Json(name = "end") override val prevToken: String? = null,
+        @Json(name = "chunk") override val events: List<Event> = emptyList(),
+        @Json(name = "state") override val stateEvents: List<Event> = emptyList()
+) : TokenChunkEvent
\ No newline at end of file
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEvent.kt
index 9e25da14b6..bf5fdd9990 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEvent.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEvent.kt
@@ -1,13 +1,10 @@
 package im.vector.matrix.android.internal.session.room.timeline
 
-import com.squareup.moshi.Json
-import com.squareup.moshi.JsonClass
 import im.vector.matrix.android.api.session.events.model.Event
 
-@JsonClass(generateAdapter = true)
-internal data class TokenChunkEvent(
-        @Json(name = "start") val nextToken: String? = null,
-        @Json(name = "end") val prevToken: String? = null,
-        @Json(name = "chunk") val events: List<Event> = emptyList(),
-        @Json(name = "state") val stateEvents: List<Event> = emptyList()
-)
\ No newline at end of file
+internal interface TokenChunkEvent {
+    val nextToken: String?
+    val prevToken: String?
+    val events: List<Event>
+    val stateEvents: List<Event>
+}
\ No newline at end of file
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt
new file mode 100644
index 0000000000..7114815705
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt
@@ -0,0 +1,73 @@
+package im.vector.matrix.android.internal.session.room.timeline
+
+import arrow.core.Try
+import com.zhuinden.monarchy.Monarchy
+import im.vector.matrix.android.internal.database.helper.*
+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.query.find
+import im.vector.matrix.android.internal.database.query.findAllIncludingEvents
+import im.vector.matrix.android.internal.database.query.where
+import im.vector.matrix.android.internal.util.tryTransactionSync
+import io.realm.kotlin.createObject
+
+
+internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {
+
+    fun insertInDb(receivedChunk: TokenChunkEvent,
+                   roomId: String,
+                   direction: PaginationDirection): Try<Unit> {
+
+        return monarchy
+                .tryTransactionSync { realm ->
+                    val roomEntity = RoomEntity.where(realm, roomId).findFirst()
+                            ?: throw IllegalStateException("You shouldn't use this method without a room")
+
+                    // We create a new chunk with prev and next token as a base
+                    // In case of permalink, we may not encounter other chunks, so it can be added
+                    // By default, it's an unlinked chunk
+                    val newChunk = realm.createObject<ChunkEntity>().apply {
+                        prevToken = receivedChunk.prevToken
+                        nextToken = receivedChunk.nextToken
+                    }
+                    newChunk.addAll(receivedChunk.events, direction, isUnlinked = true)
+
+                    // The current chunk is the one we will keep all along the merge process.
+                    var currentChunk = newChunk
+                    val prevChunk = ChunkEntity.find(realm, roomId, nextToken = receivedChunk.prevToken)
+                    val nextChunk = ChunkEntity.find(realm, roomId, prevToken = receivedChunk.nextToken)
+
+                    // We always merge the bottom chunk into top chunk, so we are always merging backwards
+                    if (prevChunk != null) {
+                        newChunk.merge(prevChunk, PaginationDirection.BACKWARDS)
+                        roomEntity.deleteOnCascade(prevChunk)
+                    }
+                    if (nextChunk != null) {
+                        nextChunk.merge(newChunk, PaginationDirection.BACKWARDS)
+                        roomEntity.deleteOnCascade(newChunk)
+                        currentChunk = nextChunk
+                    }
+                    val newEventIds = receivedChunk.events.mapNotNull { it.eventId }
+                    ChunkEntity
+                            .findAllIncludingEvents(realm, newEventIds)
+                            .filter { it != currentChunk }
+                            .forEach { overlapped ->
+                                if (direction == PaginationDirection.BACKWARDS) {
+                                    currentChunk.merge(overlapped, PaginationDirection.BACKWARDS)
+                                    roomEntity.deleteOnCascade(overlapped)
+                                } else {
+                                    overlapped.merge(currentChunk, PaginationDirection.BACKWARDS)
+                                    roomEntity.deleteOnCascade(currentChunk)
+                                    currentChunk = overlapped
+                                }
+                            }
+                    roomEntity.addOrUpdate(currentChunk)
+
+                    // TODO : there is an issue with the pagination sending unwanted room member events
+                    val isUnlinked = currentChunk.isUnlinked()
+                    roomEntity.addStateEvents(receivedChunk.stateEvents, isUnlinked = isUnlinked)
+                }
+    }
+
+
+}
\ No newline at end of file