From 06dcf75a3211e6d057684715ccb5d8d18d2a68b3 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 13 Aug 2019 12:06:49 +0200 Subject: [PATCH] Read receipts: fix not appearing RR --- .../mapper/ReadReceiptsSummaryMapper.kt | 4 - .../database/mapper/TimelineEventMapper.kt | 12 +- .../session/room/read/DefaultReadService.kt | 2 + .../session/room/timeline/DefaultTimeline.kt | 112 ++++----------- .../room/timeline/DefaultTimelineService.kt | 4 +- .../timeline/TimelineHiddenReadReceipts.kt | 130 ++++++++++++++++++ .../session/sync/ReadReceiptHandler.kt | 4 +- 7 files changed, 170 insertions(+), 98 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/ReadReceiptsSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/ReadReceiptsSummaryMapper.kt index 1f53d1b4..9fa9fc01 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/ReadReceiptsSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/ReadReceiptsSummaryMapper.kt @@ -17,7 +17,6 @@ package im.vector.matrix.android.internal.database.mapper import im.vector.matrix.android.api.session.room.model.ReadReceipt -import im.vector.matrix.android.internal.database.model.ReadReceiptEntityFields import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity import im.vector.matrix.android.internal.database.model.UserEntity import im.vector.matrix.android.internal.database.query.where @@ -40,9 +39,6 @@ internal class ReadReceiptsSummaryMapper @Inject constructor(@SessionDatabase pr ?: return@mapNotNull null ReadReceipt(user.asDomain(), it.originServerTs.toLong()) } - .sortedByDescending { - it.originServerTs - } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt index 42365d7e..84c860a8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt @@ -26,9 +26,11 @@ import javax.inject.Inject internal class TimelineEventMapper @Inject constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper) { fun map(timelineEventEntity: TimelineEventEntity, correctedReadReceipts: List? = null): TimelineEvent { - val readReceipts = correctedReadReceipts ?: timelineEventEntity.readReceipts?.let { - readReceiptsSummaryMapper.map(it) - } + val readReceipts = correctedReadReceipts ?: timelineEventEntity.readReceipts + ?.let { + readReceiptsSummaryMapper.map(it) + } + return TimelineEvent( root = timelineEventEntity.root?.asDomain() ?: Event("", timelineEventEntity.eventId), @@ -38,7 +40,9 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS senderName = timelineEventEntity.senderName, isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName, senderAvatar = timelineEventEntity.senderAvatar, - readReceipts = readReceipts ?: emptyList() + readReceipts = readReceipts?.sortedByDescending { + it.originServerTs + } ?: emptyList() ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt index 3df872bf..470668ef 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt @@ -96,6 +96,8 @@ internal class DefaultReadService @Inject constructor(private val roomId: String return Transformations.map(liveEntity) { realmResults -> realmResults.firstOrNull()?.let { readReceiptsSummaryMapper.map(it) + }?.sortedByDescending { + it.originServerTs } ?: emptyList() } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index 059b2415..f961f9d5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.session.room.timeline -import android.util.SparseArray import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.events.model.EventType @@ -34,8 +33,6 @@ import im.vector.matrix.android.internal.database.model.ChunkEntityFields import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity 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.model.ReadReceiptsSummaryEntity -import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntityFields import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields @@ -68,7 +65,7 @@ import kotlin.collections.HashMap private const val MIN_FETCHING_COUNT = 30 private const val DISPLAY_INDEX_UNKNOWN = Int.MIN_VALUE -private const val EDIT_FILTER_LIKE = "{*\"m.relates_to\"*\"rel_type\":*\"m.replace\"*}" +internal const val EDIT_FILTER_LIKE = "{*\"m.relates_to\"*\"rel_type\":*\"m.replace\"*}" internal class DefaultTimeline( private val roomId: String, @@ -79,9 +76,9 @@ internal class DefaultTimeline( private val paginationTask: PaginationTask, private val cryptoService: CryptoService, private val timelineEventMapper: TimelineEventMapper, - private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, - private val settings: TimelineSettings -) : Timeline { + private val settings: TimelineSettings, + private val hiddenReadReceipts: TimelineHiddenReadReceipts +) : Timeline, TimelineHiddenReadReceipts.Delegate { private companion object { val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD") @@ -104,9 +101,6 @@ internal class DefaultTimeline( private lateinit var liveEvents: RealmResults private lateinit var eventRelations: RealmResults - private var hiddenReadReceipts: RealmResults? = null - private val correctedReadReceiptsEventByIndex = SparseArray() - private val correctedReadReceiptsByEvent = HashMap>() private var roomEntity: RoomEntity? = null @@ -118,10 +112,8 @@ internal class DefaultTimeline( private val backwardsPaginationState = AtomicReference(PaginationState()) private val forwardsPaginationState = AtomicReference(PaginationState()) - private val timelineID = UUID.randomUUID().toString() - private val eventDecryptor = TimelineEventDecryptor(realmConfiguration, timelineID, cryptoService) private val eventsChangeListener = OrderedRealmCollectionChangeListener> { results, changeSet -> @@ -162,7 +154,7 @@ internal class DefaultTimeline( builtEventsIdMap[eventId]?.let { builtIndex -> //Update an existing event builtEvents[builtIndex]?.let { te -> - builtEvents[builtIndex] = timelineEventMapper.map(eventEntity, correctedReadReceiptsByEvent[te.root.eventId]) + builtEvents[builtIndex] = timelineEventMapper.map(eventEntity, hiddenReadReceipts.correctedReadReceipts(te.root.eventId)) hasChanged = true } } @@ -192,56 +184,8 @@ internal class DefaultTimeline( postSnapshot() } - private val hiddenReadReceiptsListener = OrderedRealmCollectionChangeListener> { collection, changeSet -> - var hasChange = false - changeSet.deletions.forEach { - val eventId = correctedReadReceiptsEventByIndex[it] - val timelineEvent = liveEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, eventId).findFirst() - builtEventsIdMap[eventId]?.let { builtIndex -> - builtEvents[builtIndex]?.let { te -> - builtEvents[builtIndex] = te.copy(readReceipts = readReceiptsSummaryMapper.map(timelineEvent?.readReceipts)) - hasChange = true - } - } - } - correctedReadReceiptsEventByIndex.clear() - correctedReadReceiptsByEvent.clear() - val loadedReadReceipts = collection.where().greaterThan("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.DISPLAY_INDEX}", prevDisplayIndex).findAll() - loadedReadReceipts.forEachIndexed { index, summary -> - val timelineEvent = summary?.timelineEvent?.firstOrNull() - val displayIndex = timelineEvent?.root?.displayIndex - if (displayIndex != null) { - val firstDisplayedEvent = liveEvents.where() - .sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING) - .lessThan(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, displayIndex) - .findFirst() - if (firstDisplayedEvent != null) { - correctedReadReceiptsEventByIndex.put(index, firstDisplayedEvent.eventId) - correctedReadReceiptsByEvent.getOrPut(firstDisplayedEvent.eventId, { - readReceiptsSummaryMapper.map(firstDisplayedEvent.readReceipts).toMutableList() - }).addAll( - readReceiptsSummaryMapper.map(summary) - ) - } - } - } - if (correctedReadReceiptsByEvent.isNotEmpty()) { - correctedReadReceiptsByEvent.forEach { (eventId, correctedReadReceipts) -> - builtEventsIdMap[eventId]?.let { builtIndex -> - builtEvents[builtIndex]?.let { te -> - builtEvents[builtIndex] = te.copy(readReceipts = correctedReadReceipts) - hasChange = true - } - } - } - } - if (hasChange) { - postSnapshot() - } - } - -// Public methods ****************************************************************************** + // Public methods ****************************************************************************** override fun paginate(direction: Timeline.Direction, count: Int) { BACKGROUND_HANDLER.post { @@ -295,12 +239,7 @@ internal class DefaultTimeline( .findAllAsync() .also { it.addChangeListener(relationsListener) } - hiddenReadReceipts = ReadReceiptsSummaryEntity.whereInRoom(realm, roomId) - .isNotEmpty(ReadReceiptsSummaryEntityFields.TIMELINE_EVENT) - .isNotEmpty(ReadReceiptsSummaryEntityFields.READ_RECEIPTS.`$`) - .filterReceiptsWithSettings() - .findAllAsync() - .also { it.addChangeListener(hiddenReadReceiptsListener) } + hiddenReadReceipts.start(realm, liveEvents, this) isReady.set(true) } @@ -315,8 +254,8 @@ internal class DefaultTimeline( cancelableBag.cancel() roomEntity?.sendingTimelineEvents?.removeAllChangeListeners() eventRelations.removeAllChangeListeners() - hiddenReadReceipts?.removeAllChangeListeners() liveEvents.removeAllChangeListeners() + hiddenReadReceipts.dispose() backgroundRealm.getAndSet(null).also { it.close() } @@ -328,6 +267,22 @@ internal class DefaultTimeline( return hasMoreInCache(direction) || !hasReachedEnd(direction) } + // TimelineHiddenReadReceipts.Delegate + + override fun rebuildEvent(eventId: String, readReceipts: List): Boolean { + return builtEventsIdMap[eventId]?.let { builtIndex -> + //Update the relation of existing event + builtEvents[builtIndex]?.let { te -> + builtEvents[builtIndex] = te.copy(readReceipts = readReceipts) + true + } + } ?: false + } + + override fun onReadReceiptsUpdated() { + postSnapshot() + } + // Private methods ***************************************************************************** private fun hasMoreInCache(direction: Timeline.Direction): Boolean { @@ -608,7 +563,7 @@ internal class DefaultTimeline( debouncer.debounce("post_snapshot", runnable, 50) } -// Extension methods *************************************************************************** + // Extension methods *************************************************************************** private fun Timeline.Direction.toPaginationDirection(): PaginationDirection { return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS @@ -634,23 +589,6 @@ internal class DefaultTimeline( return this } - /** - * We are looking for receipts related to filtered events. So, it's the opposite of [filterEventsWithSettings] method. - */ - private fun RealmQuery.filterReceiptsWithSettings(): RealmQuery { - beginGroup() - if (settings.filterTypes) { - not().`in`("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.TYPE}", settings.allowedTypes.toTypedArray()) - } - if (settings.filterTypes && settings.filterEdits) { - or() - } - if (settings.filterEdits) { - like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", EDIT_FILTER_LIKE) - } - endGroup() - return this - } } private data class PaginationState( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt index a2a80274..82058a91 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt @@ -53,8 +53,8 @@ internal class DefaultTimelineService @Inject constructor(private val roomId: St paginationTask, cryptoService, timelineEventMapper, - readReceiptsSummaryMapper, - settings + settings, + TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings) ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt new file mode 100644 index 00000000..db34d240 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt @@ -0,0 +1,130 @@ +/* + * 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 android.util.SparseArray +import im.vector.matrix.android.api.session.room.model.ReadReceipt +import im.vector.matrix.android.api.session.room.timeline.TimelineSettings +import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper +import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity +import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntityFields +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.whereInRoom +import io.realm.OrderedRealmCollectionChangeListener +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.RealmResults + +internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, + private val roomId: String, + private val settings: TimelineSettings) { + + interface Delegate { + fun rebuildEvent(eventId: String, readReceipts: List): Boolean + fun onReadReceiptsUpdated() + } + + private val correctedReadReceiptsEventByIndex = SparseArray() + private val correctedReadReceiptsByEvent = HashMap>() + + private lateinit var hiddenReadReceipts: RealmResults + private lateinit var liveEvents: RealmResults + private lateinit var delegate: Delegate + + private val hiddenReadReceiptsListener = OrderedRealmCollectionChangeListener> { collection, changeSet -> + var hasChange = false + changeSet.deletions.forEach { + val eventId = correctedReadReceiptsEventByIndex[it] + val timelineEvent = liveEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, eventId).findFirst() + val readReceipts = readReceiptsSummaryMapper.map(timelineEvent?.readReceipts) + hasChange = hasChange || delegate.rebuildEvent(eventId, readReceipts) + } + correctedReadReceiptsEventByIndex.clear() + correctedReadReceiptsByEvent.clear() + hiddenReadReceipts.forEachIndexed { index, summary -> + val timelineEvent = summary?.timelineEvent?.firstOrNull() + val displayIndex = timelineEvent?.root?.displayIndex + if (displayIndex != null) { + val firstDisplayedEvent = liveEvents.where() + .lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, displayIndex) + .findFirst() + + if (firstDisplayedEvent != null) { + correctedReadReceiptsEventByIndex.put(index, firstDisplayedEvent.eventId) + correctedReadReceiptsByEvent.getOrPut(firstDisplayedEvent.eventId, { + readReceiptsSummaryMapper.map(firstDisplayedEvent.readReceipts).toMutableList() + }).addAll( + readReceiptsSummaryMapper.map(summary) + ) + } + } + } + if (correctedReadReceiptsByEvent.isNotEmpty()) { + correctedReadReceiptsByEvent.forEach { (eventId, correctedReadReceipts) -> + val sortedReadReceipts = correctedReadReceipts.sortedByDescending { + it.originServerTs + } + hasChange = hasChange || delegate.rebuildEvent(eventId, sortedReadReceipts) + } + } + if (hasChange) { + delegate.onReadReceiptsUpdated() + } + } + + + fun start(realm: Realm, liveEvents: RealmResults, delegate: Delegate) { + this.liveEvents = liveEvents + this.delegate = delegate + this.hiddenReadReceipts = ReadReceiptsSummaryEntity.whereInRoom(realm, roomId) + .isNotEmpty(ReadReceiptsSummaryEntityFields.TIMELINE_EVENT) + .isNotEmpty(ReadReceiptsSummaryEntityFields.READ_RECEIPTS.`$`) + .filterReceiptsWithSettings() + .findAllAsync() + .also { it.addChangeListener(hiddenReadReceiptsListener) } + } + + fun dispose() { + this.hiddenReadReceipts?.removeAllChangeListeners() + } + + fun correctedReadReceipts(eventId: String?): List? { + return correctedReadReceiptsByEvent[eventId] + } + + + /** + * We are looking for receipts related to filtered events. So, it's the opposite of [filterEventsWithSettings] method. + */ + private fun RealmQuery.filterReceiptsWithSettings(): RealmQuery { + beginGroup() + if (settings.filterTypes) { + not().`in`("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.TYPE}", settings.allowedTypes.toTypedArray()) + } + if (settings.filterTypes && settings.filterEdits) { + or() + } + if (settings.filterEdits) { + like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", EDIT_FILTER_LIKE) + } + endGroup() + return this + } + + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/ReadReceiptHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/ReadReceiptHandler.kt index 7c752e49..e61e81dd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/ReadReceiptHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/ReadReceiptHandler.kt @@ -78,7 +78,9 @@ internal class ReadReceiptHandler @Inject constructor() { for ((eventId, receiptDict) in content) { val userIdsDict = receiptDict[READ_KEY] ?: continue val readReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst() - ?: realm.createObject(ReadReceiptsSummaryEntity::class.java, eventId) + ?: realm.createObject(ReadReceiptsSummaryEntity::class.java, eventId).apply { + this.roomId = roomId + } for ((userId, paramsDict) in userIdsDict) { val ts = paramsDict[TIMESTAMP_KEY] ?: 0.0