Sync/pagination: get a working version

This commit is contained in:
Ganard 2020-01-28 14:46:26 +01:00
parent 15b0bea870
commit bf7c53ecab
13 changed files with 157 additions and 154 deletions

View file

@ -26,10 +26,12 @@ import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
import im.vector.matrix.android.internal.database.query.find
import im.vector.matrix.android.internal.database.query.getOrCreate
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.extensions.assertIsManaged
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
import io.realm.Sort
import io.realm.kotlin.createObject
internal fun ChunkEntity.deleteOnCascade() {
@ -38,14 +40,38 @@ internal fun ChunkEntity.deleteOnCascade() {
this.deleteFromRealm()
}
internal fun ChunkEntity.merge(chunkToMerge: ChunkEntity, direction: PaginationDirection) {
assertIsManaged()
val eventsToMerge: List<TimelineEventEntity>
if (direction == PaginationDirection.FORWARDS) {
this.nextToken = chunkToMerge.nextToken
this.isLastForward = chunkToMerge.isLastForward
eventsToMerge = chunkToMerge.timelineEvents.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
} else {
this.prevToken = chunkToMerge.prevToken
this.isLastBackward = chunkToMerge.isLastBackward
eventsToMerge = chunkToMerge.timelineEvents.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
}
return eventsToMerge
.forEach {
if (timelineEvents.find(it.eventId) == null) {
it.displayIndex = nextDisplayIndex(direction)
this.timelineEvents.add(it)
}
}
}
internal fun ChunkEntity.addTimelineEvent(roomId: String,
eventEntity: EventEntity,
direction: PaginationDirection,
roomMemberEvent: Event?): TimelineEventEntity {
roomMemberEvent: Event?) {
val eventId = eventEntity.eventId
if (timelineEvents.find(eventId) != null) {
return
}
val displayIndex = nextDisplayIndex(direction)
val localId = TimelineEventEntity.nextId(realm)
val eventId = eventEntity.eventId
val senderId = eventEntity.sender ?: ""
val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst()
@ -69,24 +95,23 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String,
}
}
val timelineEventEntity = TimelineEventEntity().also {
it.localId = localId
it.root = eventEntity
it.eventId = eventId
it.roomId = roomId
it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
it.readReceipts = readReceiptsSummaryEntity
it.displayIndex = displayIndex
}
if (roomMemberEvent != null) {
val roomMemberContent = roomMemberEvent.content.toModel<RoomMemberContent>()
timelineEventEntity.senderAvatar = roomMemberContent?.avatarUrl
timelineEventEntity.senderName = roomMemberContent?.displayName
timelineEventEntity.isUniqueDisplayName = false
timelineEventEntity.senderMembershipEventId = roomMemberEvent.eventId
val timelineEventEntity = realm.createObject<TimelineEventEntity>().apply {
this.localId = localId
this.root = eventEntity
this.eventId = eventId
this.roomId = roomId
this.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
this.readReceipts = readReceiptsSummaryEntity
this.displayIndex = displayIndex
if (roomMemberEvent != null) {
val roomMemberContent = roomMemberEvent.content.toModel<RoomMemberContent>()
this.senderAvatar = roomMemberContent?.avatarUrl
this.senderName = roomMemberContent?.displayName
this.isUniqueDisplayName = false
this.senderMembershipEventId = roomMemberEvent.eventId
}
}
timelineEvents.add(timelineEventEntity)
return timelineEventEntity
}
internal fun ChunkEntity.nextDisplayIndex(direction: PaginationDirection): Int {

View file

@ -26,9 +26,7 @@ internal open class ChunkEntity(@Index var prevToken: String? = null,
@Index var nextToken: String? = null,
var timelineEvents: RealmList<TimelineEventEntity> = RealmList(),
@Index var isLastForward: Boolean = false,
@Index var isLastBackward: Boolean = false,
var backwardsDisplayIndex: Int? = null,
var forwardsDisplayIndex: Int? = null
@Index var isLastBackward: Boolean = false
) : RealmObject() {
fun identifier() = "${prevToken}_$nextToken"

View file

@ -41,12 +41,6 @@ internal open class EventEntity(@PrimaryKey var eventId: String = "",
var decryptionErrorCode: String? = null
) : RealmObject() {
enum class LinkFilterMode {
LINKED_ONLY,
UNLINKED_ONLY,
BOTH
}
private var sendStateStr: String = SendState.UNKNOWN.name
var sendState: SendState
@ -59,9 +53,6 @@ internal open class EventEntity(@PrimaryKey var eventId: String = "",
companion object
@LinkingObjects("root")
val timelineEventEntity: RealmResults<TimelineEventEntity>? = null
fun setDecryptionResult(result: MXEventDecryptionResult) {
val decryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
@ -72,6 +63,5 @@ internal open class EventEntity(@PrimaryKey var eventId: String = "",
val adapter = MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java)
decryptionResultJson = adapter.toJson(decryptionResult)
decryptionErrorCode = null
timelineEventEntity?.firstOrNull()?.root = this
}
}

View file

@ -23,7 +23,7 @@ import io.realm.annotations.LinkingObjects
import io.realm.annotations.PrimaryKey
internal open class TimelineEventEntity(var localId: Long = 0,
@PrimaryKey var eventId: String = "",
@Index var eventId: String = "",
@Index var roomId: String = "",
@Index var displayIndex: Int = 0,
var root: EventEntity? = null,

View file

@ -17,12 +17,10 @@
package im.vector.matrix.android.internal.database.query
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 io.realm.Realm
import io.realm.RealmList
import io.realm.RealmQuery
import io.realm.Sort
import io.realm.kotlin.where
internal fun EventEntity.Companion.where(realm: Realm, eventId: String): RealmQuery<EventEntity> {

View file

@ -18,7 +18,6 @@ package im.vector.matrix.android.internal.database.query
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.internal.database.model.*
import im.vector.matrix.android.internal.database.model.EventEntity.LinkFilterMode.*
import io.realm.*
import io.realm.kotlin.where

View file

@ -129,9 +129,6 @@ internal abstract class RoomModule {
@Binds
abstract fun bindGetContextOfEventTask(task: DefaultGetContextOfEventTask): GetContextOfEventTask
@Binds
abstract fun bindClearUnlinkedEventsTask(task: DefaultClearUnlinkedEventsTask): ClearUnlinkedEventsTask
@Binds
abstract fun bindPaginationTask(task: DefaultPaginationTask): PaginationTask

View file

@ -48,21 +48,23 @@ internal class RoomSummaryUpdater @Inject constructor(
private val monarchy: Monarchy) {
// TODO: maybe allow user of SDK to give that list
private val PREVIEWABLE_TYPES = listOf(
EventType.MESSAGE,
EventType.STATE_ROOM_NAME,
EventType.STATE_ROOM_TOPIC,
EventType.STATE_ROOM_MEMBER,
EventType.STATE_ROOM_HISTORY_VISIBILITY,
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
EventType.CALL_ANSWER,
EventType.ENCRYPTED,
EventType.STATE_ROOM_ENCRYPTION,
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
EventType.STICKER,
EventType.STATE_ROOM_CREATE
)
companion object {
val PREVIEWABLE_TYPES = listOf(
EventType.MESSAGE,
EventType.STATE_ROOM_NAME,
EventType.STATE_ROOM_TOPIC,
EventType.STATE_ROOM_MEMBER,
EventType.STATE_ROOM_HISTORY_VISIBILITY,
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
EventType.CALL_ANSWER,
EventType.ENCRYPTED,
EventType.STATE_ROOM_ENCRYPTION,
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
EventType.STICKER,
EventType.STATE_ROOM_CREATE
)
}
fun update(realm: Realm,
roomId: String,

View file

@ -1,48 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.room.timeline
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.internal.database.helper.deleteOnCascade
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.ChunkEntityFields
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.awaitTransaction
import javax.inject.Inject
internal interface ClearUnlinkedEventsTask : Task<ClearUnlinkedEventsTask.Params, Unit> {
data class Params(val roomId: String)
}
internal class DefaultClearUnlinkedEventsTask @Inject constructor() : ClearUnlinkedEventsTask {
override suspend fun execute(params: ClearUnlinkedEventsTask.Params) {
return
/*monarchy.awaitTransaction { localRealm ->
val unlinkedChunks = ChunkEntity
.where(localRealm, roomId = params.roomId)
.findAll()
unlinkedChunks.forEach {
it.deleteOnCascade()
}
}
*/
}
}

View file

@ -27,8 +27,7 @@ internal interface GetContextOfEventTask : Task<GetContextOfEventTask.Params, To
data class Params(
val roomId: String,
val eventId: String,
val limit: Int
val eventId: String
)
}
@ -39,10 +38,12 @@ internal class DefaultGetContextOfEventTask @Inject constructor(
private val eventBus: EventBus
) : GetContextOfEventTask {
override suspend fun execute(params: GetContextOfEventTask.Params): TokenChunkEventPersistor.Result {
val filter = filterRepository.getRoomFilter()
val response = executeRequest<EventContextResponse>(eventBus) {
apiCall = roomAPI.getContextOfEvent(params.roomId, params.eventId, params.limit, filter)
// We are limiting the response to the event with eventId to be sure we don't have any issue with potential merging process.
apiCall = roomAPI.getContextOfEvent(params.roomId, params.eventId, 1, filter)
}
return tokenChunkEventPersistor.insertInDb(response, params.roomId, PaginationDirection.BACKWARDS)
}

View file

@ -71,7 +71,6 @@ internal class DefaultTimeline(
private val realmConfiguration: RealmConfiguration,
private val taskExecutor: TaskExecutor,
private val contextOfEventTask: GetContextOfEventTask,
private val clearUnlinkedEventsTask: ClearUnlinkedEventsTask,
private val paginationTask: PaginationTask,
private val cryptoService: CryptoService,
private val timelineEventMapper: TimelineEventMapper,
@ -225,9 +224,6 @@ internal class DefaultTimeline(
}
eventDecryptor.destroy()
}
clearUnlinkedEventsTask
.configureWith(ClearUnlinkedEventsTask.Params(roomId))
.executeBy(taskExecutor)
}
}
@ -653,7 +649,7 @@ internal class DefaultTimeline(
}
private fun fetchEvent(eventId: String) {
val params = GetContextOfEventTask.Params(roomId, eventId, settings.initialSize)
val params = GetContextOfEventTask.Params(roomId, eventId)
cancelableBag += contextOfEventTask.configureWith(params) {
callback = object : MatrixCallback<TokenChunkEventPersistor.Result> {
override fun onFailure(failure: Throwable) {

View file

@ -44,8 +44,7 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
private val cryptoService: CryptoService,
private val paginationTask: PaginationTask,
private val timelineEventMapper: TimelineEventMapper,
private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper,
private val clearUnlinkedEventsTask: ClearUnlinkedEventsTask
private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper
) : TimelineService {
@AssistedInject.Factory
@ -60,7 +59,6 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
realmConfiguration = monarchy.realmConfiguration,
taskExecutor = taskExecutor,
contextOfEventTask = contextOfEventTask,
clearUnlinkedEventsTask = clearUnlinkedEventsTask,
paginationTask = paginationTask,
cryptoService = cryptoService,
timelineEventMapper = timelineEventMapper,

View file

@ -22,17 +22,27 @@ import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.internal.database.helper.addOrUpdate
import im.vector.matrix.android.internal.database.helper.addTimelineEvent
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.mapper.asDomain
import im.vector.matrix.android.internal.database.mapper.toEntity
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
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.findLastLiveChunkFromRoom
import im.vector.matrix.android.internal.database.query.getOrNull
import im.vector.matrix.android.internal.database.query.latestEvent
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
import im.vector.matrix.android.internal.util.awaitTransaction
import io.realm.Realm
import io.realm.RealmList
import io.realm.kotlin.createObject
import timber.log.Timber
import javax.inject.Inject
@ -117,8 +127,6 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
monarchy
.awaitTransaction { realm ->
Timber.v("Start persisting ${receivedChunk.events.size} events in $roomId towards $direction")
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId)
val nextToken: String?
val prevToken: String?
@ -144,47 +152,9 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
?: ChunkEntity.create(realm, prevToken, nextToken)
if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) {
Timber.v("Reach end of $roomId")
currentChunk.isLastBackward = true
handleReachEnd(realm, roomId, direction, currentChunk)
} else {
Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}")
val roomMemberEventsByUser = HashMap<String, Event?>()
val eventList = if (direction == PaginationDirection.FORWARDS) {
receivedChunk.events
} else {
receivedChunk.events.asReversed()
}
val stateEvents = receivedChunk.stateEvents
for (stateEvent in stateEvents) {
if (stateEvent.type == EventType.STATE_ROOM_MEMBER && stateEvent.stateKey != null && !stateEvent.isRedacted()) {
roomMemberEventsByUser[stateEvent.stateKey] = stateEvent
}
}
val eventEntities = ArrayList<EventEntity>(eventList.size)
for (event in eventList) {
if (event.eventId == null || event.senderId == null) {
continue
}
val eventEntity = event.toEntity(roomId, SendState.SYNCED).also {
realm.copyToRealmOrUpdate(it)
}
if(direction == PaginationDirection.FORWARDS){
eventEntities.add(eventEntity)
}else {
eventEntities.add(0, eventEntity)
}
if (event.type == EventType.STATE_ROOM_MEMBER && event.stateKey != null && !event.isRedacted()) {
roomMemberEventsByUser[event.stateKey] = event
}
}
for (eventEntity in eventEntities) {
val senderId = eventEntity.sender ?: continue
val roomMemberEvent = roomMemberEventsByUser.getOrPut(senderId) {
CurrentStateEventEntity.getOrNull(realm, roomId, senderId, EventType.STATE_ROOM_MEMBER)?.root?.asDomain()
}
currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberEvent)
}
roomEntity.addOrUpdate(currentChunk)
handlePagination(realm, roomId, direction, receivedChunk, currentChunk)
}
}
return if (receivedChunk.events.isEmpty()) {
@ -197,4 +167,81 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
Result.SUCCESS
}
}
private fun handleReachEnd(realm: Realm, roomId: String, direction: PaginationDirection, currentChunk: ChunkEntity) {
Timber.v("Reach end of $roomId")
if (direction == PaginationDirection.FORWARDS) {
val currentLiveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)
if (currentChunk != currentLiveChunk) {
currentChunk.isLastForward = true
currentLiveChunk?.deleteOnCascade()
RoomSummaryEntity.where(realm, roomId).findFirst()?.apply {
latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, filterTypes = RoomSummaryUpdater.PREVIEWABLE_TYPES)
}
}
} else {
currentChunk.isLastBackward = true
}
}
private fun handlePagination(
realm: Realm,
roomId: String,
direction: PaginationDirection,
receivedChunk: TokenChunkEvent,
currentChunk: ChunkEntity
) {
Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}")
val roomMemberEventsByUser = HashMap<String, Event?>()
val eventList = if (direction == PaginationDirection.FORWARDS) {
receivedChunk.events
} else {
receivedChunk.events.asReversed()
}
val stateEvents = receivedChunk.stateEvents
for (stateEvent in stateEvents) {
if (stateEvent.type == EventType.STATE_ROOM_MEMBER && stateEvent.stateKey != null && !stateEvent.isRedacted()) {
roomMemberEventsByUser[stateEvent.stateKey] = stateEvent
}
}
val eventIds = ArrayList<String>(eventList.size)
val eventEntities = ArrayList<EventEntity>(eventList.size)
for (event in eventList) {
if (event.eventId == null || event.senderId == null) {
continue
}
eventIds.add(event.eventId)
val eventEntity = event.toEntity(roomId, SendState.SYNCED).let {
realm.copyToRealmOrUpdate(it)
}
if (direction == PaginationDirection.FORWARDS) {
eventEntities.add(eventEntity)
} else {
eventEntities.add(0, eventEntity)
}
if (event.type == EventType.STATE_ROOM_MEMBER && event.stateKey != null && !event.isRedacted()) {
roomMemberEventsByUser[event.stateKey] = event
}
}
for (eventEntity in eventEntities) {
val senderId = eventEntity.sender ?: continue
val roomMemberEvent = roomMemberEventsByUser.getOrPut(senderId) {
CurrentStateEventEntity.getOrNull(realm, roomId, senderId, EventType.STATE_ROOM_MEMBER)?.root?.asDomain()
}
currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberEvent)
}
val chunks = ChunkEntity.findAllIncludingEvents(realm, eventIds)
val chunksToDelete = ArrayList<ChunkEntity>()
chunks.forEach {
if (it != currentChunk) {
currentChunk.merge(it, direction)
chunksToDelete.add(it)
}
}
chunksToDelete.forEach {
it.deleteFromRealm()
}
RoomEntity.where(realm, roomId).findFirst()?.addOrUpdate(currentChunk)
}
}