/* * 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.FilterContent 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 // Deletion here means we don't have any readReceipts for the given hidden events changeSet.deletions.forEach { val eventId = correctedReadReceiptsEventByIndex[it] val timelineEvent = liveEvents.where() .equalTo(TimelineEventEntityFields.EVENT_ID, eventId) .findFirst() // We are rebuilding the corresponding event with only his own RR val readReceipts = readReceiptsSummaryMapper.map(timelineEvent?.readReceipts) hasChange = delegate.rebuildEvent(eventId, readReceipts) || hasChange } correctedReadReceiptsEventByIndex.clear() correctedReadReceiptsByEvent.clear() hiddenReadReceipts.forEachIndexed { index, summary -> val timelineEvent = summary?.timelineEvent?.firstOrNull() val displayIndex = timelineEvent?.root?.displayIndex if (displayIndex != null) { // Then we are looking for the first displayable event after the hidden one val firstDisplayedEvent = liveEvents.where() .lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, displayIndex) .findFirst() // If we find one, we should if (firstDisplayedEvent != null) { correctedReadReceiptsEventByIndex.put(index, firstDisplayedEvent.eventId) correctedReadReceiptsByEvent .getOrPut(firstDisplayedEvent.eventId, { ArrayList(readReceiptsSummaryMapper.map(firstDisplayedEvent.readReceipts)) }) .addAll(readReceiptsSummaryMapper.map(summary)) } } } if (correctedReadReceiptsByEvent.isNotEmpty()) { correctedReadReceiptsByEvent.forEach { (eventId, correctedReadReceipts) -> val sortedReadReceipts = correctedReadReceipts.sortedByDescending { it.originServerTs } hasChange = delegate.rebuildEvent(eventId, sortedReadReceipts) || hasChange } } if (hasChange) { delegate.onReadReceiptsUpdated() } } fun start(realm: Realm, liveEvents: RealmResults, delegate: Delegate) { this.liveEvents = liveEvents this.delegate = delegate // We are looking for read receipts set on hidden events. // We only accept those with a timelineEvent (so coming from pagination/sync). 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 [DefaultTimeline.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}", FilterContent.EDIT_TYPE) } endGroup() return this } }