Read receipts: fix not appearing RR

This commit is contained in:
ganfra 2019-08-13 12:06:49 +02:00
parent 21deb2551d
commit 06dcf75a32
7 changed files with 170 additions and 98 deletions

View File

@ -17,7 +17,6 @@
package im.vector.matrix.android.internal.database.mapper package im.vector.matrix.android.internal.database.mapper
import im.vector.matrix.android.api.session.room.model.ReadReceipt 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.ReadReceiptsSummaryEntity
import im.vector.matrix.android.internal.database.model.UserEntity import im.vector.matrix.android.internal.database.model.UserEntity
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
@ -40,9 +39,6 @@ internal class ReadReceiptsSummaryMapper @Inject constructor(@SessionDatabase pr
?: return@mapNotNull null ?: return@mapNotNull null
ReadReceipt(user.asDomain(), it.originServerTs.toLong()) ReadReceipt(user.asDomain(), it.originServerTs.toLong())
} }
.sortedByDescending {
it.originServerTs
}
} }
} }

View File

@ -26,9 +26,11 @@ import javax.inject.Inject
internal class TimelineEventMapper @Inject constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper) { internal class TimelineEventMapper @Inject constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper) {
fun map(timelineEventEntity: TimelineEventEntity, correctedReadReceipts: List<ReadReceipt>? = null): TimelineEvent { fun map(timelineEventEntity: TimelineEventEntity, correctedReadReceipts: List<ReadReceipt>? = null): TimelineEvent {
val readReceipts = correctedReadReceipts ?: timelineEventEntity.readReceipts?.let { val readReceipts = correctedReadReceipts ?: timelineEventEntity.readReceipts
readReceiptsSummaryMapper.map(it) ?.let {
} readReceiptsSummaryMapper.map(it)
}
return TimelineEvent( return TimelineEvent(
root = timelineEventEntity.root?.asDomain() root = timelineEventEntity.root?.asDomain()
?: Event("", timelineEventEntity.eventId), ?: Event("", timelineEventEntity.eventId),
@ -38,7 +40,9 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS
senderName = timelineEventEntity.senderName, senderName = timelineEventEntity.senderName,
isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName, isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName,
senderAvatar = timelineEventEntity.senderAvatar, senderAvatar = timelineEventEntity.senderAvatar,
readReceipts = readReceipts ?: emptyList() readReceipts = readReceipts?.sortedByDescending {
it.originServerTs
} ?: emptyList()
) )
} }

View File

@ -96,6 +96,8 @@ internal class DefaultReadService @Inject constructor(private val roomId: String
return Transformations.map(liveEntity) { realmResults -> return Transformations.map(liveEntity) { realmResults ->
realmResults.firstOrNull()?.let { realmResults.firstOrNull()?.let {
readReceiptsSummaryMapper.map(it) readReceiptsSummaryMapper.map(it)
}?.sortedByDescending {
it.originServerTs
} ?: emptyList() } ?: emptyList()
} }
} }

View File

@ -16,7 +16,6 @@
package im.vector.matrix.android.internal.session.room.timeline 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.MatrixCallback
import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.events.model.EventType 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.EventAnnotationsSummaryEntity
import im.vector.matrix.android.internal.database.model.EventEntity 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.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.RoomEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity 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.model.TimelineEventEntityFields
@ -68,7 +65,7 @@ import kotlin.collections.HashMap
private const val MIN_FETCHING_COUNT = 30 private const val MIN_FETCHING_COUNT = 30
private const val DISPLAY_INDEX_UNKNOWN = Int.MIN_VALUE 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( internal class DefaultTimeline(
private val roomId: String, private val roomId: String,
@ -79,9 +76,9 @@ internal class DefaultTimeline(
private val paginationTask: PaginationTask, private val paginationTask: PaginationTask,
private val cryptoService: CryptoService, private val cryptoService: CryptoService,
private val timelineEventMapper: TimelineEventMapper, private val timelineEventMapper: TimelineEventMapper,
private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, private val settings: TimelineSettings,
private val settings: TimelineSettings private val hiddenReadReceipts: TimelineHiddenReadReceipts
) : Timeline { ) : Timeline, TimelineHiddenReadReceipts.Delegate {
private companion object { private companion object {
val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD") val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD")
@ -104,9 +101,6 @@ internal class DefaultTimeline(
private lateinit var liveEvents: RealmResults<TimelineEventEntity> private lateinit var liveEvents: RealmResults<TimelineEventEntity>
private lateinit var eventRelations: RealmResults<EventAnnotationsSummaryEntity> private lateinit var eventRelations: RealmResults<EventAnnotationsSummaryEntity>
private var hiddenReadReceipts: RealmResults<ReadReceiptsSummaryEntity>? = null
private val correctedReadReceiptsEventByIndex = SparseArray<String>()
private val correctedReadReceiptsByEvent = HashMap<String, MutableList<ReadReceipt>>()
private var roomEntity: RoomEntity? = null private var roomEntity: RoomEntity? = null
@ -118,10 +112,8 @@ internal class DefaultTimeline(
private val backwardsPaginationState = AtomicReference(PaginationState()) private val backwardsPaginationState = AtomicReference(PaginationState())
private val forwardsPaginationState = AtomicReference(PaginationState()) private val forwardsPaginationState = AtomicReference(PaginationState())
private val timelineID = UUID.randomUUID().toString() private val timelineID = UUID.randomUUID().toString()
private val eventDecryptor = TimelineEventDecryptor(realmConfiguration, timelineID, cryptoService) private val eventDecryptor = TimelineEventDecryptor(realmConfiguration, timelineID, cryptoService)
private val eventsChangeListener = OrderedRealmCollectionChangeListener<RealmResults<TimelineEventEntity>> { results, changeSet -> private val eventsChangeListener = OrderedRealmCollectionChangeListener<RealmResults<TimelineEventEntity>> { results, changeSet ->
@ -162,7 +154,7 @@ internal class DefaultTimeline(
builtEventsIdMap[eventId]?.let { builtIndex -> builtEventsIdMap[eventId]?.let { builtIndex ->
//Update an existing event //Update an existing event
builtEvents[builtIndex]?.let { te -> 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 hasChanged = true
} }
} }
@ -192,56 +184,8 @@ internal class DefaultTimeline(
postSnapshot() postSnapshot()
} }
private val hiddenReadReceiptsListener = OrderedRealmCollectionChangeListener<RealmResults<ReadReceiptsSummaryEntity>> { 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) { // Public methods ******************************************************************************
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 ******************************************************************************
override fun paginate(direction: Timeline.Direction, count: Int) { override fun paginate(direction: Timeline.Direction, count: Int) {
BACKGROUND_HANDLER.post { BACKGROUND_HANDLER.post {
@ -295,12 +239,7 @@ internal class DefaultTimeline(
.findAllAsync() .findAllAsync()
.also { it.addChangeListener(relationsListener) } .also { it.addChangeListener(relationsListener) }
hiddenReadReceipts = ReadReceiptsSummaryEntity.whereInRoom(realm, roomId) hiddenReadReceipts.start(realm, liveEvents, this)
.isNotEmpty(ReadReceiptsSummaryEntityFields.TIMELINE_EVENT)
.isNotEmpty(ReadReceiptsSummaryEntityFields.READ_RECEIPTS.`$`)
.filterReceiptsWithSettings()
.findAllAsync()
.also { it.addChangeListener(hiddenReadReceiptsListener) }
isReady.set(true) isReady.set(true)
} }
@ -315,8 +254,8 @@ internal class DefaultTimeline(
cancelableBag.cancel() cancelableBag.cancel()
roomEntity?.sendingTimelineEvents?.removeAllChangeListeners() roomEntity?.sendingTimelineEvents?.removeAllChangeListeners()
eventRelations.removeAllChangeListeners() eventRelations.removeAllChangeListeners()
hiddenReadReceipts?.removeAllChangeListeners()
liveEvents.removeAllChangeListeners() liveEvents.removeAllChangeListeners()
hiddenReadReceipts.dispose()
backgroundRealm.getAndSet(null).also { backgroundRealm.getAndSet(null).also {
it.close() it.close()
} }
@ -328,6 +267,22 @@ internal class DefaultTimeline(
return hasMoreInCache(direction) || !hasReachedEnd(direction) return hasMoreInCache(direction) || !hasReachedEnd(direction)
} }
// TimelineHiddenReadReceipts.Delegate
override fun rebuildEvent(eventId: String, readReceipts: List<ReadReceipt>): 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 methods *****************************************************************************
private fun hasMoreInCache(direction: Timeline.Direction): Boolean { private fun hasMoreInCache(direction: Timeline.Direction): Boolean {
@ -608,7 +563,7 @@ internal class DefaultTimeline(
debouncer.debounce("post_snapshot", runnable, 50) debouncer.debounce("post_snapshot", runnable, 50)
} }
// Extension methods *************************************************************************** // Extension methods ***************************************************************************
private fun Timeline.Direction.toPaginationDirection(): PaginationDirection { private fun Timeline.Direction.toPaginationDirection(): PaginationDirection {
return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS
@ -634,23 +589,6 @@ internal class DefaultTimeline(
return this return this
} }
/**
* We are looking for receipts related to filtered events. So, it's the opposite of [filterEventsWithSettings] method.
*/
private fun RealmQuery<ReadReceiptsSummaryEntity>.filterReceiptsWithSettings(): RealmQuery<ReadReceiptsSummaryEntity> {
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( private data class PaginationState(

View File

@ -53,8 +53,8 @@ internal class DefaultTimelineService @Inject constructor(private val roomId: St
paginationTask, paginationTask,
cryptoService, cryptoService,
timelineEventMapper, timelineEventMapper,
readReceiptsSummaryMapper, settings,
settings TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings)
) )
} }

View File

@ -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<ReadReceipt>): Boolean
fun onReadReceiptsUpdated()
}
private val correctedReadReceiptsEventByIndex = SparseArray<String>()
private val correctedReadReceiptsByEvent = HashMap<String, MutableList<ReadReceipt>>()
private lateinit var hiddenReadReceipts: RealmResults<ReadReceiptsSummaryEntity>
private lateinit var liveEvents: RealmResults<TimelineEventEntity>
private lateinit var delegate: Delegate
private val hiddenReadReceiptsListener = OrderedRealmCollectionChangeListener<RealmResults<ReadReceiptsSummaryEntity>> { 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<TimelineEventEntity>, 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<ReadReceipt>? {
return correctedReadReceiptsByEvent[eventId]
}
/**
* We are looking for receipts related to filtered events. So, it's the opposite of [filterEventsWithSettings] method.
*/
private fun RealmQuery<ReadReceiptsSummaryEntity>.filterReceiptsWithSettings(): RealmQuery<ReadReceiptsSummaryEntity> {
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
}
}

View File

@ -78,7 +78,9 @@ internal class ReadReceiptHandler @Inject constructor() {
for ((eventId, receiptDict) in content) { for ((eventId, receiptDict) in content) {
val userIdsDict = receiptDict[READ_KEY] ?: continue val userIdsDict = receiptDict[READ_KEY] ?: continue
val readReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst() 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) { for ((userId, paramsDict) in userIdsDict) {
val ts = paramsDict[TIMESTAMP_KEY] ?: 0.0 val ts = paramsDict[TIMESTAMP_KEY] ?: 0.0