diff --git a/.idea/dictionaries/ganfra.xml b/.idea/dictionaries/ganfra.xml
index 1ca7a97a..7e1fdcdd 100644
--- a/.idea/dictionaries/ganfra.xml
+++ b/.idea/dictionaries/ganfra.xml
@@ -5,6 +5,7 @@
coroutine
merlins
moshi
+ persistor
synchronizer
untimelined
diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt
index 5f5810bb..c4197eef 100644
--- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt
+++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt
@@ -17,6 +17,10 @@ class TimelineEventController(private val roomId: String,
EpoxyAsyncUtil.getAsyncBackgroundHandler()
) {
+ init {
+ setFilterDuplicates(true)
+ }
+
private val pagedListCallback = object : PagedList.Callback() {
override fun onChanged(position: Int, count: Int) {
buildSnapshotList()
diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle
index 5417dc13..49a9a3b7 100644
--- a/matrix-sdk-android/build.gradle
+++ b/matrix-sdk-android/build.gradle
@@ -20,6 +20,7 @@ repositories {
android {
compileSdkVersion 28
+ testOptions.unitTests.includeAndroidResources = true
defaultConfig {
minSdkVersion 21
@@ -45,6 +46,7 @@ dependencies {
def support_version = '28.0.0'
def moshi_version = '1.8.0'
def lifecycle_version = "1.1.1"
+ def powermock_version = "2.0.0-RC.4"
implementation fileTree(dir: 'libs', include: ['*.aar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
@@ -94,7 +96,14 @@ dependencies {
testImplementation 'junit:junit:4.12'
+ testImplementation 'org.robolectric:robolectric:4.0.2'
+ testImplementation 'org.robolectric:shadows-support-v4:3.0'
+ testImplementation "io.mockk:mockk:1.8.13.kotlin13"
+ testImplementation 'org.amshove.kluent:kluent-android:1.44'
+
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
+ androidTestImplementation 'org.amshove.kluent:kluent-android:1.44'
+
}
diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/ExampleInstrumentedTest.java b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/ExampleInstrumentedTest.java
deleted file mode 100644
index f9b3c62a..00000000
--- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/ExampleInstrumentedTest.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package im.vector.matrix.android;
-
-import android.content.Context;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import static org.junit.Assert.*;
-
-/**
- * Instrumented test, which will execute on an Android device.
- *
- * @see Testing documentation
- */
-@RunWith(AndroidJUnit4.class)
-public class ExampleInstrumentedTest {
- @Test
- public void useAppContext() {
- // Context of the app under test.
- Context appContext = InstrumentationRegistry.getTargetContext();
-
- assertEquals("im.vector.matrix.android.test", appContext.getPackageName());
- }
-}
diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/InstrumentedTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/InstrumentedTest.kt
new file mode 100644
index 00000000..c726b7eb
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/InstrumentedTest.kt
@@ -0,0 +1,15 @@
+package im.vector.matrix.android
+
+import android.content.Context
+import android.support.test.InstrumentationRegistry
+import java.io.File
+
+abstract class InstrumentedTest {
+ fun context(): Context {
+ return InstrumentationRegistry.getTargetContext()
+ }
+
+ fun cacheDir(): File {
+ return context().cacheDir
+ }
+}
\ No newline at end of file
diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt
new file mode 100644
index 00000000..b1c89935
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt
@@ -0,0 +1,177 @@
+package im.vector.matrix.android.session.room.timeline
+
+import com.zhuinden.monarchy.Monarchy
+import im.vector.matrix.android.InstrumentedTest
+import im.vector.matrix.android.api.session.events.model.Event
+import im.vector.matrix.android.api.session.events.model.EventType
+import im.vector.matrix.android.internal.database.helper.add
+import im.vector.matrix.android.internal.database.helper.addAll
+import im.vector.matrix.android.internal.database.helper.isUnlinked
+import im.vector.matrix.android.internal.database.helper.lastStateIndex
+import im.vector.matrix.android.internal.database.helper.merge
+import im.vector.matrix.android.internal.database.model.ChunkEntity
+import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
+import io.realm.Realm
+import io.realm.RealmConfiguration
+import io.realm.kotlin.createObject
+import org.amshove.kluent.shouldBeFalse
+import org.amshove.kluent.shouldBeTrue
+import org.amshove.kluent.shouldEqual
+import org.junit.Before
+import org.junit.Test
+import kotlin.random.Random
+
+
+internal class ChunkEntityTest : InstrumentedTest() {
+
+ private lateinit var monarchy: Monarchy
+
+ @Before
+ fun setup() {
+ Realm.init(context())
+ val testConfig = RealmConfiguration.Builder().inMemory().name("test-realm").build()
+ monarchy = Monarchy.Builder().setRealmConfiguration(testConfig).build()
+ }
+
+
+ @Test
+ fun add_shouldAdd_whenNotAlreadyIncluded() {
+ monarchy.runTransactionSync { realm ->
+ val chunk: ChunkEntity = realm.createObject()
+ val fakeEvent = createFakeEvent(false)
+ chunk.add(fakeEvent, PaginationDirection.FORWARDS)
+ chunk.events.size shouldEqual 1
+ }
+ }
+
+ @Test
+ fun add_shouldNotAdd_whenAlreadyIncluded() {
+ monarchy.runTransactionSync { realm ->
+ val chunk: ChunkEntity = realm.createObject()
+ val fakeEvent = createFakeEvent(false)
+ chunk.add(fakeEvent, PaginationDirection.FORWARDS)
+ chunk.add(fakeEvent, PaginationDirection.FORWARDS)
+ chunk.events.size shouldEqual 1
+ }
+ }
+
+ @Test
+ fun add_shouldStateIndexIncremented_whenStateEventIsAddedForward() {
+ monarchy.runTransactionSync { realm ->
+ val chunk: ChunkEntity = realm.createObject()
+ val fakeEvent = createFakeEvent(true)
+ chunk.add(fakeEvent, PaginationDirection.FORWARDS)
+ chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 1
+ }
+ }
+
+ @Test
+ fun add_shouldStateIndexNotIncremented_whenNoStateEventIsAdded() {
+ monarchy.runTransactionSync { realm ->
+ val chunk: ChunkEntity = realm.createObject()
+ val fakeEvent = createFakeEvent(false)
+ chunk.add(fakeEvent, PaginationDirection.FORWARDS)
+ chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 0
+ }
+ }
+
+ @Test
+ fun addAll_shouldStateIndexIncremented_whenStateEventsAreAddedForward() {
+ monarchy.runTransactionSync { realm ->
+ val chunk: ChunkEntity = realm.createObject()
+ val fakeEvents = createFakeListOfEvents(30)
+ val numberOfStateEvents = fakeEvents.filter { it.isStateEvent() }.size
+ chunk.addAll(fakeEvents, PaginationDirection.FORWARDS)
+ chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual numberOfStateEvents
+ }
+ }
+
+ @Test
+ fun addAll_shouldStateIndexDecremented_whenStateEventsAreAddedBackward() {
+ monarchy.runTransactionSync { realm ->
+ val chunk: ChunkEntity = realm.createObject()
+ val fakeEvents = createFakeListOfEvents(30)
+ val numberOfStateEvents = fakeEvents.filter { it.isStateEvent() }.size
+ val lastIsState = fakeEvents.last().isStateEvent()
+ val expectedStateIndex = if (lastIsState) -numberOfStateEvents + 1 else -numberOfStateEvents
+ chunk.addAll(fakeEvents, PaginationDirection.BACKWARDS)
+ chunk.lastStateIndex(PaginationDirection.BACKWARDS) shouldEqual expectedStateIndex
+ }
+ }
+
+ @Test
+ fun merge_shouldAddEvents_whenMergingBackward() {
+ monarchy.runTransactionSync { realm ->
+ val chunk1: ChunkEntity = realm.createObject()
+ val chunk2: ChunkEntity = realm.createObject()
+ chunk1.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
+ chunk2.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
+ chunk1.merge(chunk2, PaginationDirection.BACKWARDS)
+ chunk1.events.size shouldEqual 60
+ }
+ }
+
+ @Test
+ fun merge_shouldEventsBeLinked_whenMergingLinkedWithUnlinked() {
+ monarchy.runTransactionSync { realm ->
+ val chunk1: ChunkEntity = realm.createObject()
+ val chunk2: ChunkEntity = realm.createObject()
+ chunk1.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
+ chunk2.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = false)
+ chunk1.merge(chunk2, PaginationDirection.BACKWARDS)
+ chunk1.isUnlinked().shouldBeFalse()
+ }
+ }
+
+ @Test
+ fun merge_shouldEventsBeUnlinked_whenMergingUnlinkedWithUnlinked() {
+ monarchy.runTransactionSync { realm ->
+ val chunk1: ChunkEntity = realm.createObject()
+ val chunk2: ChunkEntity = realm.createObject()
+ chunk1.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
+ chunk2.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
+ chunk1.merge(chunk2, PaginationDirection.BACKWARDS)
+ chunk1.isUnlinked().shouldBeTrue()
+ }
+ }
+
+ @Test
+ fun merge_shouldPrevTokenMerged_whenMergingForwards() {
+ monarchy.runTransactionSync { realm ->
+ val chunk1: ChunkEntity = realm.createObject()
+ val chunk2: ChunkEntity = realm.createObject()
+ val prevToken = "prev_token"
+ chunk1.prevToken = prevToken
+ chunk1.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
+ chunk2.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
+ chunk1.merge(chunk2, PaginationDirection.FORWARDS)
+ chunk1.prevToken shouldEqual prevToken
+ }
+ }
+
+ @Test
+ fun merge_shouldNextTokenMerged_whenMergingBackwards() {
+ monarchy.runTransactionSync { realm ->
+ val chunk1: ChunkEntity = realm.createObject()
+ val chunk2: ChunkEntity = realm.createObject()
+ val nextToken = "next_token"
+ chunk1.nextToken = nextToken
+ chunk1.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
+ chunk2.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
+ chunk1.merge(chunk2, PaginationDirection.BACKWARDS)
+ chunk1.nextToken shouldEqual nextToken
+ }
+ }
+
+
+ private fun createFakeListOfEvents(size: Int = 10): List {
+ return (0 until size).map { createFakeEvent(Random.nextBoolean()) }
+ }
+
+ private fun createFakeEvent(asStateEvent: Boolean = false): Event {
+ val eventId = Random.nextLong(System.currentTimeMillis()).toString()
+ val type = if (asStateEvent) EventType.STATE_ROOM_NAME else EventType.MESSAGE
+ return Event(type, eventId)
+ }
+
+}
\ No newline at end of file
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt
index 15ba170e..f8d8207a 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt
@@ -2,36 +2,55 @@ package im.vector.matrix.android.internal.database.helper
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
-import im.vector.matrix.android.internal.database.mapper.asDomain
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.EventEntityFields
import im.vector.matrix.android.internal.database.query.fastContains
-import im.vector.matrix.android.internal.database.query.find
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
import io.realm.Sort
+internal fun ChunkEntity.deleteOnCascade() {
+ this.events.deleteAllFromRealm()
+ this.deleteFromRealm()
+}
-internal fun ChunkEntity.merge(chunkEntity: ChunkEntity,
+// By default if a chunk is empty we consider it unlinked
+internal fun ChunkEntity.isUnlinked(): Boolean {
+ return events.where().equalTo(EventEntityFields.IS_UNLINKED, false).findAll().isEmpty()
+}
+
+internal fun ChunkEntity.merge(chunkToMerge: ChunkEntity,
direction: PaginationDirection) {
+ val isChunkToMergeUnlinked = chunkToMerge.isUnlinked()
+ val isCurrentChunkUnlinked = this.isUnlinked()
+ val isUnlinked = isCurrentChunkUnlinked && isChunkToMergeUnlinked
- chunkEntity.events.forEach {
- addOrUpdate(it.asDomain(), direction)
+ if (isCurrentChunkUnlinked && !isChunkToMergeUnlinked) {
+ this.events.forEach { it.isUnlinked = false }
}
+ val eventsToMerge: List
if (direction == PaginationDirection.FORWARDS) {
- nextToken = chunkEntity.nextToken
+ this.nextToken = chunkToMerge.nextToken
+ this.isLast = chunkToMerge.isLast
+ eventsToMerge = chunkToMerge.events.reversed()
} else {
- prevToken = chunkEntity.prevToken
+ this.prevToken = chunkToMerge.prevToken
+ eventsToMerge = chunkToMerge.events
+ }
+ eventsToMerge.forEach {
+ add(it, direction, isUnlinked = isUnlinked)
}
}
internal fun ChunkEntity.addAll(events: List,
direction: PaginationDirection,
- stateIndexOffset: Int = 0) {
+ stateIndexOffset: Int = 0,
+ isUnlinked: Boolean = false) {
events.forEach { event ->
- addOrUpdate(event, direction, stateIndexOffset)
+ add(event, direction, stateIndexOffset, isUnlinked)
}
}
@@ -39,19 +58,27 @@ internal fun ChunkEntity.updateDisplayIndexes() {
events.forEachIndexed { index, eventEntity -> eventEntity.displayIndex = index }
}
-internal fun ChunkEntity.addOrUpdate(event: Event,
- direction: PaginationDirection,
- stateIndexOffset: Int = 0) {
+internal fun ChunkEntity.add(event: Event,
+ direction: PaginationDirection,
+ stateIndexOffset: Int = 0,
+ isUnlinked: Boolean = false) {
+ add(event.asEntity(), direction, stateIndexOffset, isUnlinked)
+}
+
+internal fun ChunkEntity.add(eventEntity: EventEntity,
+ direction: PaginationDirection,
+ stateIndexOffset: Int = 0,
+ isUnlinked: Boolean = false) {
if (!isManaged) {
throw IllegalStateException("Chunk entity should be managed to use fast contains")
}
- if (event.eventId == null) {
+ if (eventEntity.eventId.isEmpty() || events.fastContains(eventEntity.eventId)) {
return
}
var currentStateIndex = lastStateIndex(direction, defaultValue = stateIndexOffset)
- if (direction == PaginationDirection.FORWARDS && event.isStateEvent()) {
+ if (direction == PaginationDirection.FORWARDS && EventType.isStateEvent(eventEntity.type)) {
currentStateIndex += 1
} else if (direction == PaginationDirection.BACKWARDS && events.isNotEmpty()) {
val lastEventType = events.last()?.type ?: ""
@@ -60,20 +87,15 @@ internal fun ChunkEntity.addOrUpdate(event: Event,
}
}
- if (!events.fastContains(event.eventId)) {
- val eventEntity = event.asEntity()
- eventEntity.stateIndex = currentStateIndex
- val position = if (direction == PaginationDirection.FORWARDS) 0 else this.events.size
- events.add(position, eventEntity)
- } else {
- val eventEntity = events.find(event.eventId)
- eventEntity?.stateIndex = currentStateIndex
- }
+ eventEntity.stateIndex = currentStateIndex
+ eventEntity.isUnlinked = isUnlinked
+ val position = if (direction == PaginationDirection.FORWARDS) 0 else this.events.size
+ events.add(position, eventEntity)
}
internal fun ChunkEntity.lastStateIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {
return when (direction) {
- PaginationDirection.FORWARDS -> events.where().sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING).findFirst()?.stateIndex
- PaginationDirection.BACKWARDS -> events.where().sort(EventEntityFields.STATE_INDEX, Sort.ASCENDING).findFirst()?.stateIndex
- } ?: defaultValue
+ PaginationDirection.FORWARDS -> events.where().sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING).findFirst()?.stateIndex
+ PaginationDirection.BACKWARDS -> events.where().sort(EventEntityFields.STATE_INDEX, Sort.ASCENDING).findFirst()?.stateIndex
+ } ?: defaultValue
}
\ No newline at end of file
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt
index 07018c35..4df958b4 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt
@@ -8,8 +8,7 @@ import im.vector.matrix.android.internal.database.model.RoomEntity
internal fun RoomEntity.deleteOnCascade(chunkEntity: ChunkEntity) {
chunks.remove(chunkEntity)
- chunkEntity.events.deleteAllFromRealm()
- chunkEntity.deleteFromRealm()
+ chunkEntity.deleteOnCascade()
}
internal fun RoomEntity.addOrUpdate(chunkEntity: ChunkEntity) {
@@ -19,7 +18,9 @@ internal fun RoomEntity.addOrUpdate(chunkEntity: ChunkEntity) {
}
}
-internal fun RoomEntity.addStateEvents(stateEvents: List, stateIndex: Int = Int.MIN_VALUE) {
+internal fun RoomEntity.addStateEvents(stateEvents: List,
+ stateIndex: Int = Int.MIN_VALUE,
+ isUnlinked: Boolean = false) {
if (!isManaged) {
throw IllegalStateException("Chunk entity should be managed to use fast contains")
}
@@ -29,6 +30,7 @@ internal fun RoomEntity.addStateEvents(stateEvents: List, stateIndex: Int
}
val eventEntity = event.asEntity()
eventEntity.stateIndex = stateIndex
+ eventEntity.isUnlinked = isUnlinked
untimelinedStateEvents.add(eventEntity)
}
}
\ No newline at end of file
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt
index 96ba986a..2ea581bf 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt
@@ -14,9 +14,16 @@ internal open class EventEntity(var eventId: String = "",
var age: Long? = 0,
var redacts: String? = null,
var stateIndex: Int = 0,
- var displayIndex: Int = 0
+ var displayIndex: Int = 0,
+ var isUnlinked: Boolean = false
) : RealmObject() {
+ enum class LinkFilterMode {
+ LINKED_ONLY,
+ UNLINKED_ONLY,
+ BOTH
+ }
+
companion object {
const val DEFAULT_STATE_INDEX = Int.MIN_VALUE
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt
index e3b071a2..2035af90 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt
@@ -6,6 +6,7 @@ import im.vector.matrix.android.internal.database.model.RoomEntityFields
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.RealmResults
+import io.realm.kotlin.createObject
import io.realm.kotlin.where
internal fun ChunkEntity.Companion.where(realm: Realm, roomId: String): RealmQuery {
@@ -34,4 +35,11 @@ internal fun ChunkEntity.Companion.findAllIncludingEvents(realm: Realm, eventIds
return realm.where()
.`in`(ChunkEntityFields.EVENTS.EVENT_ID, eventIds.toTypedArray())
.findAll()
+}
+
+internal fun ChunkEntity.Companion.create(realm: Realm, prevToken: String?, nextToken: String?): ChunkEntity {
+ return realm.createObject().apply {
+ this.prevToken = prevToken
+ this.nextToken = nextToken
+ }
}
\ No newline at end of file
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt
index 91a828c7..7b582287 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt
@@ -2,6 +2,7 @@ package im.vector.matrix.android.internal.database.query
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.LinkFilterMode.*
import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.model.RoomEntityFields
import io.realm.Realm
@@ -15,7 +16,10 @@ internal fun EventEntity.Companion.where(realm: Realm, eventId: String): RealmQu
.equalTo(EventEntityFields.EVENT_ID, eventId)
}
-internal fun EventEntity.Companion.where(realm: Realm, roomId: String? = null, type: String? = null): RealmQuery {
+internal fun EventEntity.Companion.where(realm: Realm,
+ roomId: String? = null,
+ type: String? = null,
+ linkFilterMode: EventEntity.LinkFilterMode = LINKED_ONLY): RealmQuery {
val query = realm.where()
if (roomId != null) {
query.beginGroup()
@@ -27,7 +31,11 @@ internal fun EventEntity.Companion.where(realm: Realm, roomId: String? = null, t
if (type != null) {
query.equalTo(EventEntityFields.TYPE, type)
}
- return query
+ return when (linkFilterMode) {
+ LINKED_ONLY -> query.equalTo(EventEntityFields.IS_UNLINKED, false)
+ UNLINKED_ONLY -> query.equalTo(EventEntityFields.IS_UNLINKED, true)
+ BOTH -> query
+ }
}
internal fun RealmQuery.next(from: Int? = null, strict: Boolean = true): EventEntity? {
@@ -61,6 +69,7 @@ internal fun RealmList.find(eventId: String): EventEntity? {
return this.where().equalTo(EventEntityFields.EVENT_ID, eventId).findFirst()
}
-internal fun RealmList.fastContains(eventId: String): Boolean {
+internal fun RealmList.
+ fastContains(eventId: String): Boolean {
return this.find(eventId) != null
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt
index 4b9ddb66..4dbc8a8c 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt
@@ -14,6 +14,7 @@ import im.vector.matrix.android.internal.session.room.RoomAvatarResolver
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
import im.vector.matrix.android.internal.session.room.members.RoomDisplayNameResolver
import im.vector.matrix.android.internal.session.room.members.RoomMemberDisplayNameResolver
+import im.vector.matrix.android.internal.util.md5
import io.realm.RealmConfiguration
import org.koin.dsl.context.ModuleDefinition
import org.koin.dsl.module.Module
@@ -31,12 +32,13 @@ internal class SessionModule(private val sessionParams: SessionParams) : Module
scope(DefaultSession.SCOPE) {
val context = get()
- val directory = File(context.filesDir, sessionParams.credentials.userId)
+ val childPath = sessionParams.credentials.userId.md5()
+ val directory = File(context.filesDir, childPath)
RealmConfiguration.Builder()
.directory(directory)
.name("disk_store.realm")
- .deleteRealmIfMigrationNeeded()
+ .inMemory()
.build()
}
@@ -47,7 +49,7 @@ internal class SessionModule(private val sessionParams: SessionParams) : Module
}
scope(DefaultSession.SCOPE) {
- val retrofitBuilder = get() as Retrofit.Builder
+ val retrofitBuilder = get()
retrofitBuilder
.baseUrl(sessionParams.homeServerConnectionConfig.homeServerUri.toString())
.build()
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 28c9776f..7c17f49f 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
+ ): Call
/**
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 e2604ded..9a7e897e 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,9 +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.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
@@ -31,10 +29,18 @@ class RoomModule : Module {
LoadRoomMembersRequest(get(), get(), get())
}
+ scope(DefaultSession.SCOPE) {
+ TokenChunkEventPersistor(get())
+ }
+
scope(DefaultSession.SCOPE) {
PaginationRequest(get(), get(), get())
}
+ scope(DefaultSession.SCOPE) {
+ GetContextOfEventRequest(get(), get(), get())
+ }
+
scope(DefaultSession.SCOPE) {
val sessionParams = get()
EventFactory(sessionParams.credentials)
@@ -43,10 +49,9 @@ class RoomModule : Module {
factory { (roomId: String) ->
val helper = PagingRequestHelper(Executors.newSingleThreadExecutor())
val timelineBoundaryCallback = TimelineBoundaryCallback(roomId, get(), get(), helper)
- DefaultTimelineHolder(roomId, get(), timelineBoundaryCallback) as TimelineHolder
+ DefaultTimelineHolder(roomId, get(), timelineBoundaryCallback, get()) as TimelineHolder
}
-
factory { (roomId: String) ->
DefaultSendService(roomId, get(), get()) as SendService
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMemberExtractor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMemberExtractor.kt
index 08012cb5..4d473ff3 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMemberExtractor.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMemberExtractor.kt
@@ -16,22 +16,26 @@ internal class RoomMemberExtractor(private val realm: Realm,
fun extractFrom(event: EventEntity): RoomMember? {
val sender = event.sender ?: return null
+ // If the event is unlinked we want to fetch unlinked state events
+ val unlinked = event.isUnlinked
// When stateIndex is negative, we try to get the next stateEvent prevContent()
// If prevContent is null we fallback to the Int.MIN state events content()
return if (event.stateIndex <= 0) {
- baseQuery(realm, roomId, sender).next(from = event.stateIndex)?.asDomain()?.prevContent()
- ?: baseQuery(realm, roomId, sender).last(since = event.stateIndex)?.asDomain()?.content()
+ baseQuery(realm, roomId, sender, unlinked).next(from = event.stateIndex)?.asDomain()?.prevContent()
+ ?: baseQuery(realm, roomId, sender, unlinked).last(since = event.stateIndex)?.asDomain()?.content()
} else {
- baseQuery(realm, roomId, sender).last(since = event.stateIndex)?.asDomain()?.content()
+ baseQuery(realm, roomId, sender, unlinked).last(since = event.stateIndex)?.asDomain()?.content()
}
}
private fun baseQuery(realm: Realm,
roomId: String,
- sender: String): RealmQuery {
+ sender: String,
+ isUnlinked: Boolean): RealmQuery {
+ val filterMode = if (isUnlinked) EventEntity.LinkFilterMode.UNLINKED_ONLY else EventEntity.LinkFilterMode.LINKED_ONLY
return EventEntity
- .where(realm, roomId = roomId, type = EventType.STATE_ROOM_MEMBER)
+ .where(realm, roomId = roomId, type = EventType.STATE_ROOM_MEMBER, linkFilterMode = filterMode)
.equalTo(EventEntityFields.STATE_KEY, sender)
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt
index 57e1fc6c..a0feebc0 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt
@@ -7,7 +7,7 @@ import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.SendService
import im.vector.matrix.android.api.session.room.send.EventFactory
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.updateDisplayIndexes
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
@@ -33,7 +33,7 @@ internal class DefaultSendService(private val roomId: String,
monarchy.tryTransactionAsync { realm ->
val chunkEntity = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)
?: return@tryTransactionAsync
- chunkEntity.addOrUpdate(event, PaginationDirection.FORWARDS)
+ chunkEntity.add(event, PaginationDirection.FORWARDS)
chunkEntity.updateDisplayIndexes()
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineHolder.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineHolder.kt
index 5f24e155..ba7acc2a 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineHolder.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineHolder.kt
@@ -4,6 +4,7 @@ import android.arch.lifecycle.LiveData
import android.arch.paging.LivePagedListBuilder
import android.arch.paging.PagedList
import com.zhuinden.monarchy.Monarchy
+import im.vector.matrix.android.api.MatrixCallback
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
@@ -13,6 +14,7 @@ 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.query.where
import im.vector.matrix.android.internal.session.events.interceptor.MessageEventInterceptor
+import im.vector.matrix.android.internal.util.tryTransactionSync
import io.realm.Realm
import io.realm.RealmQuery
@@ -20,7 +22,8 @@ private const val PAGE_SIZE = 30
internal class DefaultTimelineHolder(private val roomId: String,
private val monarchy: Monarchy,
- private val boundaryCallback: TimelineBoundaryCallback
+ private val boundaryCallback: TimelineBoundaryCallback,
+ private val contextOfEventRequest: GetContextOfEventRequest
) : TimelineHolder {
private val eventInterceptors = ArrayList()
@@ -31,8 +34,9 @@ internal class DefaultTimelineHolder(private val roomId: String,
}
override fun timeline(eventId: String?): LiveData> {
+ clearUnlinkedEvents()
if (eventId != null) {
- fetchEventIfNeeded()
+ fetchEventIfNeeded(eventId)
}
val realmDataSourceFactory = monarchy.createDataSourceFactory {
buildDataSourceFactoryQuery(it, eventId)
@@ -60,18 +64,38 @@ internal class DefaultTimelineHolder(private val roomId: String,
return monarchy.findAllPagedWithChanges(realmDataSourceFactory, livePagedListBuilder)
}
- private fun fetchEventIfNeeded() {
+ private fun clearUnlinkedEvents() {
+ monarchy.tryTransactionSync { realm ->
+ val unlinkedEvents = EventEntity
+ .where(realm, roomId = roomId)
+ .equalTo(EventEntityFields.IS_UNLINKED, true)
+ .findAll()
+ unlinkedEvents.deleteAllFromRealm()
+ }
+ }
+ private fun fetchEventIfNeeded(eventId: String) {
+ if (!isEventPersisted(eventId)) {
+ contextOfEventRequest.execute(roomId, eventId, object : MatrixCallback {})
+ }
+ }
+
+ private fun isEventPersisted(eventId: String): Boolean {
+ var isEventPersisted = false
+ monarchy.doWithRealm {
+ isEventPersisted = EventEntity.where(it, eventId = eventId).findFirst() != null
+ }
+ return isEventPersisted
}
private fun buildDataSourceFactoryQuery(realm: Realm, eventId: String?): RealmQuery {
val query = if (eventId == null) {
EventEntity
- .where(realm, roomId = roomId)
+ .where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.LINKED_ONLY)
.equalTo("${EventEntityFields.CHUNK}.${ChunkEntityFields.IS_LAST}", true)
} else {
EventEntity
- .where(realm, roomId = roomId)
+ .where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.BOTH)
.`in`("${EventEntityFields.CHUNK}.${ChunkEntityFields.EVENTS.EVENT_ID}", arrayOf(eventId))
}
return query.sort(EventEntityFields.DISPLAY_INDEX)
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 79cfac72..99fac915 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,16 +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 start: String? = null,
@Json(name = "events_before") val eventsBefore: List = emptyList(),
@Json(name = "events_after") val eventsAfter: List = emptyList(),
- @Json(name = "end") val nextToken: String? = null,
- @Json(name = "state") val stateEvents: List = emptyList()
-) {
-
- val timelineEvents: List by lazy {
- eventsBefore + event + eventsAfter
- }
+ @Json(name = "end") override val end: String? = null,
+ @Json(name = "state") override val stateEvents: List = emptyList()
+) : TokenChunkEvent {
+ override val events: List
+ 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 248c616a..868d6953 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.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.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
) {
@@ -46,54 +33,11 @@ internal class GetContextOfEventRequest(private val roomAPI: RoomAPI,
filter: String?) = withContext(coroutineDispatchers.io) {
executeRequest {
- apiCall = roomAPI.getContextOfEvent(roomId, eventId, 1, filter)
+ 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 {
- 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().apply {
- prevToken = response.prevToken
- nextToken = response.nextToken
- }
-
- currentChunk.addOrUpdate(response.event, PaginationDirection.FORWARDS)
- currentChunk.addAll(response.eventsAfter, PaginationDirection.FORWARDS)
- currentChunk.addAll(response.eventsBefore, PaginationDirection.BACKWARDS)
-
- // 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)
- }
- /*
- val eventIds = response.timelineEvents.mapNotNull { it.eventId }
- ChunkEntity
- .findAllIncludingEvents(realm, eventIds)
- .filter { it != currentChunk }
- .forEach { overlapped ->
- currentChunk.merge(overlapped, direction)
- roomEntity.deleteOnCascade(overlapped)
- }
- */
- roomEntity.addOrUpdate(currentChunk)
- roomEntity.addStateEvents(response.stateEvents)
- }
- .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/PaginationDirection.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationDirection.kt
index b65a0ca0..ffb8dd0b 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationDirection.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/PaginationDirection.kt
@@ -13,4 +13,11 @@ internal enum class PaginationDirection(val value: String) {
*/
BACKWARDS("b");
+ fun reversed(): PaginationDirection {
+ return when (this) {
+ FORWARDS -> BACKWARDS
+ BACKWARDS -> FORWARDS
+ }
+ }
+
}
\ 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 29a3e6c9..4f603b9b 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,40 +1,26 @@
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.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
) {
fun execute(roomId: String,
from: String?,
direction: PaginationDirection,
- limit: Int = 10,
+ limit: Int,
callback: MatrixCallback
): Cancelable {
val job = GlobalScope.launch(coroutineDispatchers.main) {
@@ -48,54 +34,19 @@ internal class PaginationRequest(private val roomAPI: RoomAPI,
private suspend fun execute(roomId: String,
from: String?,
direction: PaginationDirection,
- limit: Int = 10,
+ limit: Int,
filter: String?) = withContext(coroutineDispatchers.io) {
if (from == null) {
return@withContext RuntimeException("From token shouldn't be null").failure()
}
- executeRequest {
+ executeRequest {
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 {
- return monarchy
- .tryTransactionSync { realm ->
- val roomEntity = RoomEntity.where(realm, roomId).findFirst()
- ?: throw IllegalStateException("You shouldn't use this method without a room")
-
- val currentChunk = ChunkEntity.find(realm, roomId, prevToken = receivedChunk.nextToken)
- ?: realm.createObject()
-
- currentChunk.prevToken = receivedChunk.prevToken
- currentChunk.addAll(receivedChunk.events, direction)
-
- // Now, handles chunk merge
-
- val prevChunk = ChunkEntity.find(realm, roomId, nextToken = receivedChunk.prevToken)
- if (prevChunk != null) {
- currentChunk.merge(prevChunk, direction)
- roomEntity.deleteOnCascade(prevChunk)
- } else {
- val eventIds = receivedChunk.events.mapNotNull { it.eventId }
- ChunkEntity
- .findAllIncludingEvents(realm, eventIds)
- .filter { it != currentChunk }
- .forEach { overlapped ->
- currentChunk.merge(overlapped, direction)
- roomEntity.deleteOnCascade(overlapped)
- }
- }
-
- roomEntity.addOrUpdate(currentChunk)
- // TODO : there is an issue with the pagination sending unwanted room member events
- roomEntity.addStateEvents(receivedChunk.stateEvents)
- }
- .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 00000000..eb1aad80
--- /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 start: String? = null,
+ @Json(name = "end") override val end: String? = null,
+ @Json(name = "chunk") override val events: List = emptyList(),
+ @Json(name = "state") override val stateEvents: List = 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/TimelineBoundaryCallback.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineBoundaryCallback.kt
index ff06dbb5..bab802e2 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineBoundaryCallback.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineBoundaryCallback.kt
@@ -8,7 +8,6 @@ import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.query.findAllIncludingEvents
import im.vector.matrix.android.internal.util.PagingRequestHelper
import java.util.*
-import java.util.concurrent.Executor
internal class TimelineBoundaryCallback(private val roomId: String,
private val paginationRequest: PaginationRequest,
@@ -24,28 +23,37 @@ internal class TimelineBoundaryCallback(private val roomId: String,
override fun onItemAtEndLoaded(itemAtEnd: EnrichedEvent) {
helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER) {
- monarchy.doWithRealm { realm ->
- if (itemAtEnd.root.eventId == null) {
- return@doWithRealm
- }
- val chunkEntity = ChunkEntity.findAllIncludingEvents(realm, Collections.singletonList(itemAtEnd.root.eventId)).firstOrNull()
- paginationRequest.execute(roomId, chunkEntity?.prevToken, PaginationDirection.BACKWARDS, limit, callback = createCallback(it))
- }
+ runPaginationRequest(it, itemAtEnd, PaginationDirection.BACKWARDS)
}
}
override fun onItemAtFrontLoaded(itemAtFront: EnrichedEvent) {
helper.runIfNotRunning(PagingRequestHelper.RequestType.BEFORE) {
- monarchy.doWithRealm { realm ->
- if (itemAtFront.root.eventId == null) {
- return@doWithRealm
- }
- val chunkEntity = ChunkEntity.findAllIncludingEvents(realm, Collections.singletonList(itemAtFront.root.eventId)).firstOrNull()
- paginationRequest.execute(roomId, chunkEntity?.nextToken, PaginationDirection.FORWARDS, limit, callback = createCallback(it))
- }
+ runPaginationRequest(it, itemAtFront, PaginationDirection.FORWARDS)
}
}
+ private fun runPaginationRequest(requestCallback: PagingRequestHelper.Request.Callback,
+ item: EnrichedEvent,
+ direction: PaginationDirection) {
+ 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()
+ token = if (direction == PaginationDirection.FORWARDS) chunkEntity?.nextToken else chunkEntity?.prevToken
+ }
+ paginationRequest.execute(
+ roomId = roomId,
+ from = token,
+ direction = direction,
+ limit = limit,
+ callback = createCallback(requestCallback)
+ )
+ }
+
+
private fun createCallback(pagingRequestCallback: PagingRequestHelper.Request.Callback) = object : MatrixCallback {
override fun onSuccess(data: TokenChunkEvent) {
pagingRequestCallback.recordSuccess()
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 9e25da14..64995181 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 = emptyList(),
- @Json(name = "state") val stateEvents: List = emptyList()
-)
\ No newline at end of file
+internal interface TokenChunkEvent {
+ val start: String?
+ val end: String?
+ val events: List
+ val stateEvents: List
+}
\ 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 00000000..bf4e9b49
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt
@@ -0,0 +1,93 @@
+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.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.create
+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
+
+
+internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {
+
+ fun insertInDb(receivedChunk: TokenChunkEvent,
+ roomId: String,
+ direction: PaginationDirection): Try {
+
+ return monarchy
+ .tryTransactionSync { realm ->
+ val roomEntity = RoomEntity.where(realm, roomId).findFirst()
+ ?: throw IllegalStateException("You shouldn't use this method without a room")
+
+ val nextToken: String?
+ val prevToken: String?
+ if (direction == PaginationDirection.FORWARDS) {
+ nextToken = receivedChunk.end
+ prevToken = receivedChunk.start
+ } else {
+ nextToken = receivedChunk.start
+ prevToken = receivedChunk.end
+ }
+ val prevChunk = ChunkEntity.find(realm, roomId, nextToken = prevToken)
+ val nextChunk = ChunkEntity.find(realm, roomId, prevToken = nextToken)
+
+ // The current chunk is the one we will keep all along the merge process.
+ // We try to look for a chunk next to the token,
+ // otherwise we create a whole new one
+
+ var currentChunk = if (direction == PaginationDirection.FORWARDS) {
+ prevChunk?.apply { this.nextToken = nextToken }
+ ?: ChunkEntity.create(realm, prevToken, nextToken)
+ } else {
+ nextChunk?.apply { this.prevToken = prevToken }
+ ?: ChunkEntity.create(realm, prevToken, nextToken)
+ }
+
+ currentChunk.addAll(receivedChunk.events, direction, isUnlinked = currentChunk.isUnlinked())
+
+ // Then we merge chunks if needed
+ if (currentChunk != prevChunk && prevChunk != null) {
+ currentChunk = handleMerge(roomEntity, direction, currentChunk, prevChunk)
+ } else if (currentChunk != nextChunk && nextChunk != null) {
+ currentChunk = handleMerge(roomEntity, direction, currentChunk, nextChunk)
+ } else {
+ val newEventIds = receivedChunk.events.mapNotNull { it.eventId }
+ ChunkEntity
+ .findAllIncludingEvents(realm, newEventIds)
+ .filter { it != currentChunk }
+ .forEach { overlapped ->
+ currentChunk = handleMerge(roomEntity, direction, currentChunk, overlapped)
+ }
+ }
+ roomEntity.addOrUpdate(currentChunk)
+ roomEntity.addStateEvents(receivedChunk.stateEvents, isUnlinked = currentChunk.isUnlinked())
+ }
+ }
+
+ private fun handleMerge(roomEntity: RoomEntity,
+ direction: PaginationDirection,
+ currentChunk: ChunkEntity,
+ otherChunk: ChunkEntity): ChunkEntity {
+
+ // We always merge the bottom chunk into top chunk, so we are always merging backwards
+ return if (direction == PaginationDirection.BACKWARDS) {
+ currentChunk.merge(otherChunk, PaginationDirection.BACKWARDS)
+ roomEntity.deleteOnCascade(otherChunk)
+ currentChunk
+ } else {
+ otherChunk.merge(currentChunk, PaginationDirection.BACKWARDS)
+ roomEntity.deleteOnCascade(currentChunk)
+ otherChunk
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Hash.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Hash.kt
new file mode 100644
index 00000000..fbfad9c2
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Hash.kt
@@ -0,0 +1,17 @@
+package im.vector.matrix.android.internal.util
+
+import java.security.MessageDigest
+
+fun String.md5() = try {
+ val digest = MessageDigest.getInstance("md5")
+ digest.update(toByteArray())
+ val bytes = digest.digest()
+ val sb = StringBuilder()
+ for (i in bytes.indices) {
+ sb.append(String.format("%02X", bytes[i]))
+ }
+ sb.toString().toLowerCase()
+} catch (exc: Exception) {
+ // Should not happen, but just in case
+ hashCode().toString()
+}
diff --git a/matrix-sdk-android/src/test/java/im/vector/matrix/android/ExampleUnitTest.java b/matrix-sdk-android/src/test/java/im/vector/matrix/android/ExampleUnitTest.java
deleted file mode 100644
index 86ea905e..00000000
--- a/matrix-sdk-android/src/test/java/im/vector/matrix/android/ExampleUnitTest.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package im.vector.matrix.android;
-
-import org.junit.Test;
-
-import static org.junit.Assert.*;
-
-/**
- * Example local unit test, which will execute on the development machine (host).
- *
- * @see Testing documentation
- */
-public class ExampleUnitTest {
- @Test
- public void addition_isCorrect() {
- assertEquals(4, 2 + 2);
- }
-}
\ No newline at end of file