Merge branch 'feature/permalink' into develop
This commit is contained in:
commit
43a462f9cc
|
@ -5,6 +5,7 @@
|
|||
<w>coroutine</w>
|
||||
<w>merlins</w>
|
||||
<w>moshi</w>
|
||||
<w>persistor</w>
|
||||
<w>synchronizer</w>
|
||||
<w>untimelined</w>
|
||||
</words>
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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 <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
@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());
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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<Event> {
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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<EventEntity>
|
||||
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<Event>,
|
||||
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
|
||||
}
|
|
@ -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<Event>, stateIndex: Int = Int.MIN_VALUE) {
|
||||
internal fun RoomEntity.addStateEvents(stateEvents: List<Event>,
|
||||
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<Event>, stateIndex: Int
|
|||
}
|
||||
val eventEntity = event.asEntity()
|
||||
eventEntity.stateIndex = stateIndex
|
||||
eventEntity.isUnlinked = isUnlinked
|
||||
untimelinedStateEvents.add(eventEntity)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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<ChunkEntity> {
|
||||
|
@ -34,4 +35,11 @@ internal fun ChunkEntity.Companion.findAllIncludingEvents(realm: Realm, eventIds
|
|||
return realm.where<ChunkEntity>()
|
||||
.`in`(ChunkEntityFields.EVENTS.EVENT_ID, eventIds.toTypedArray())
|
||||
.findAll()
|
||||
}
|
||||
|
||||
internal fun ChunkEntity.Companion.create(realm: Realm, prevToken: String?, nextToken: String?): ChunkEntity {
|
||||
return realm.createObject<ChunkEntity>().apply {
|
||||
this.prevToken = prevToken
|
||||
this.nextToken = nextToken
|
||||
}
|
||||
}
|
|
@ -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<EventEntity> {
|
||||
internal fun EventEntity.Companion.where(realm: Realm,
|
||||
roomId: String? = null,
|
||||
type: String? = null,
|
||||
linkFilterMode: EventEntity.LinkFilterMode = LINKED_ONLY): RealmQuery<EventEntity> {
|
||||
val query = realm.where<EventEntity>()
|
||||
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<EventEntity>.next(from: Int? = null, strict: Boolean = true): EventEntity? {
|
||||
|
@ -61,6 +69,7 @@ internal fun RealmList<EventEntity>.find(eventId: String): EventEntity? {
|
|||
return this.where().equalTo(EventEntityFields.EVENT_ID, eventId).findFirst()
|
||||
}
|
||||
|
||||
internal fun RealmList<EventEntity>.fastContains(eventId: String): Boolean {
|
||||
internal fun RealmList<EventEntity>.
|
||||
fastContains(eventId: String): Boolean {
|
||||
return this.find(eventId) != null
|
||||
}
|
||||
|
|
|
@ -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<Context>()
|
||||
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<Retrofit.Builder>()
|
||||
retrofitBuilder
|
||||
.baseUrl(sessionParams.homeServerConnectionConfig.homeServerUri.toString())
|
||||
.build()
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
@ -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<SessionParams>()
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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<EventEntity> {
|
||||
sender: String,
|
||||
isUnlinked: Boolean): RealmQuery<EventEntity> {
|
||||
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)
|
||||
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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<EnrichedEventInterceptor>()
|
||||
|
@ -31,8 +34,9 @@ internal class DefaultTimelineHolder(private val roomId: String,
|
|||
}
|
||||
|
||||
override fun timeline(eventId: String?): LiveData<PagedList<EnrichedEvent>> {
|
||||
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<EventContextResponse> {})
|
||||
}
|
||||
}
|
||||
|
||||
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<EventEntity> {
|
||||
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)
|
||||
|
|
|
@ -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<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()
|
||||
) {
|
||||
|
||||
val timelineEvents: List<Event> by lazy {
|
||||
eventsBefore + event + eventsAfter
|
||||
}
|
||||
@Json(name = "end") override val end: String? = null,
|
||||
@Json(name = "state") override val stateEvents: List<Event> = emptyList()
|
||||
) : TokenChunkEvent {
|
||||
|
||||
override val events: List<Event>
|
||||
get() = listOf(event)
|
||||
|
||||
}
|
||||
|
|
|
@ -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<EventContextResponse> {
|
||||
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<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.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 }
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -13,4 +13,11 @@ internal enum class PaginationDirection(val value: String) {
|
|||
*/
|
||||
BACKWARDS("b");
|
||||
|
||||
fun reversed(): PaginationDirection {
|
||||
return when (this) {
|
||||
FORWARDS -> BACKWARDS
|
||||
BACKWARDS -> FORWARDS
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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<TokenChunkEvent>
|
||||
): 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<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")
|
||||
|
||||
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 }
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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<Event> = emptyList(),
|
||||
@Json(name = "state") override val stateEvents: List<Event> = emptyList()
|
||||
) : TokenChunkEvent
|
|
@ -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<TokenChunkEvent> {
|
||||
override fun onSuccess(data: TokenChunkEvent) {
|
||||
pagingRequestCallback.recordSuccess()
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
internal interface TokenChunkEvent {
|
||||
val start: String?
|
||||
val end: String?
|
||||
val events: List<Event>
|
||||
val stateEvents: List<Event>
|
||||
}
|
|
@ -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<Unit> {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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 <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
public class ExampleUnitTest {
|
||||
@Test
|
||||
public void addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue