diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineSettings.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineSettings.kt index 219c23eb..992cad41 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineSettings.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineSettings.kt @@ -35,5 +35,10 @@ data class TimelineSettings( /** * If [filterTypes] is true, the list of types allowed by the list. */ - val allowedTypes: List = emptyList() + val allowedTypes: List = emptyList(), + /** + * If true, will build read receipts for each event. + */ + val buildReadReceipts: Boolean = true + ) 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 84c860a8..fe98ebfb 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 @@ -25,12 +25,15 @@ 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) - } - + fun map(timelineEventEntity: TimelineEventEntity, buildReadReceipts: Boolean = true, correctedReadReceipts: List? = null): TimelineEvent { + val readReceipts = if (buildReadReceipts) { + correctedReadReceipts ?: timelineEventEntity.readReceipts + ?.let { + readReceiptsSummaryMapper.map(it) + } + } else { + null + } return TimelineEvent( root = timelineEventEntity.root?.asDomain() ?: Event("", timelineEventEntity.eventId), diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.kt new file mode 100644 index 00000000..9e6261c6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.kt @@ -0,0 +1,23 @@ +/* + * 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.database.query + +internal object FilterContent { + + internal const val EDIT_TYPE = "{*\"m.relates_to\"*\"rel_type\":*\"m.replace\"*}" + +} \ No newline at end of file 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 f961f9d5..03f5da6e 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 @@ -25,7 +25,6 @@ import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineSettings import im.vector.matrix.android.api.util.CancelableBag -import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.ChunkEntity @@ -36,6 +35,7 @@ import im.vector.matrix.android.internal.database.model.EventEntityFields 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 +import im.vector.matrix.android.internal.database.query.FilterContent import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates import im.vector.matrix.android.internal.database.query.findIncludingEvent import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom @@ -65,8 +65,6 @@ import kotlin.collections.HashMap private const val MIN_FETCHING_COUNT = 30 private const val DISPLAY_INDEX_UNKNOWN = Int.MIN_VALUE -internal const val EDIT_FILTER_LIKE = "{*\"m.relates_to\"*\"rel_type\":*\"m.replace\"*}" - internal class DefaultTimeline( private val roomId: String, private val initialEventId: String? = null, @@ -154,7 +152,7 @@ internal class DefaultTimeline( builtEventsIdMap[eventId]?.let { builtIndex -> //Update an existing event builtEvents[builtIndex]?.let { te -> - builtEvents[builtIndex] = timelineEventMapper.map(eventEntity, hiddenReadReceipts.correctedReadReceipts(te.root.eventId)) + builtEvents[builtIndex] = buildTimelineEvent(eventEntity) hasChanged = true } } @@ -239,7 +237,9 @@ internal class DefaultTimeline( .findAllAsync() .also { it.addChangeListener(relationsListener) } - hiddenReadReceipts.start(realm, liveEvents, this) + if (settings.buildReadReceipts) { + hiddenReadReceipts.start(realm, liveEvents, this) + } isReady.set(true) } @@ -255,7 +255,9 @@ internal class DefaultTimeline( roomEntity?.sendingTimelineEvents?.removeAllChangeListeners() eventRelations.removeAllChangeListeners() liveEvents.removeAllChangeListeners() - hiddenReadReceipts.dispose() + if (settings.buildReadReceipts) { + hiddenReadReceipts.dispose() + } backgroundRealm.getAndSet(null).also { it.close() } @@ -482,7 +484,7 @@ internal class DefaultTimeline( } offsetResults.forEach { eventEntity -> - val timelineEvent = timelineEventMapper.map(eventEntity) + val timelineEvent = buildTimelineEvent(eventEntity) if (timelineEvent.isEncrypted() && timelineEvent.root.mxDecryptionResult == null) { @@ -500,6 +502,12 @@ internal class DefaultTimeline( return offsetResults.size } + private fun buildTimelineEvent(eventEntity: TimelineEventEntity) = timelineEventMapper.map( + timelineEventEntity = eventEntity, + buildReadReceipts = settings.buildReadReceipts, + correctedReadReceipts = hiddenReadReceipts.correctedReadReceipts(eventEntity.eventId) + ) + /** * This has to be called on TimelineThread as it access realm live results */ @@ -584,7 +592,7 @@ internal class DefaultTimeline( `in`(TimelineEventEntityFields.ROOT.TYPE, settings.allowedTypes.toTypedArray()) } if (settings.filterEdits) { - not().like(TimelineEventEntityFields.ROOT.CONTENT, EDIT_FILTER_LIKE) + not().like(TimelineEventEntityFields.ROOT.CONTENT, FilterContent.EDIT_TYPE) } return this } 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 82058a91..441b0620 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 @@ -27,7 +27,6 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineSettings import im.vector.matrix.android.internal.database.RealmLiveData import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper -import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.task.TaskExecutor 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 index db34d240..5678677d 100644 --- 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 @@ -24,6 +24,7 @@ import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntit 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 @@ -48,11 +49,13 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu 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 = hasChange || delegate.rebuildEvent(eventId, readReceipts) + hasChange = delegate.rebuildEvent(eventId, readReceipts) || hasChange } correctedReadReceiptsEventByIndex.clear() correctedReadReceiptsByEvent.clear() @@ -60,10 +63,12 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu 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, { @@ -79,7 +84,7 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu val sortedReadReceipts = correctedReadReceipts.sortedByDescending { it.originServerTs } - hasChange = hasChange || delegate.rebuildEvent(eventId, sortedReadReceipts) + hasChange = delegate.rebuildEvent(eventId, sortedReadReceipts) || hasChange } } if (hasChange) { @@ -91,6 +96,8 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu 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.`$`) @@ -109,7 +116,7 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu /** - * We are looking for receipts related to filtered events. So, it's the opposite of [filterEventsWithSettings] method. + * We are looking for receipts related to filtered events. So, it's the opposite of [DefaultTimeline.filterEventsWithSettings] method. */ private fun RealmQuery.filterReceiptsWithSettings(): RealmQuery { beginGroup() @@ -120,7 +127,7 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu or() } if (settings.filterEdits) { - like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", EDIT_FILTER_LIKE) + like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", FilterContent.EDIT_TYPE) } endGroup() return this diff --git a/vector/src/main/java/im/vector/riotx/core/resources/UserPreferencesProvider.kt b/vector/src/main/java/im/vector/riotx/core/resources/UserPreferencesProvider.kt index 0d2c30a5..2d411f30 100644 --- a/vector/src/main/java/im/vector/riotx/core/resources/UserPreferencesProvider.kt +++ b/vector/src/main/java/im/vector/riotx/core/resources/UserPreferencesProvider.kt @@ -24,4 +24,9 @@ class UserPreferencesProvider @Inject constructor(private val vectorPreferences: fun shouldShowHiddenEvents(): Boolean { return vectorPreferences.shouldShowHiddenEvents() } + + fun shouldShowReadReceipts(): Boolean { + return vectorPreferences.showReadReceipts() + } + } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 12dace1b..1cd8cc4a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -67,7 +67,7 @@ import java.util.concurrent.TimeUnit class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: RoomDetailViewState, - userPreferencesProvider: UserPreferencesProvider, + private val userPreferencesProvider: UserPreferencesProvider, private val vectorPreferences: VectorPreferences, private val session: Session ) : VectorViewModel(initialState) { @@ -77,9 +77,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private val eventId = initialState.eventId private val displayedEventsObservable = BehaviorRelay.create() private val timelineSettings = if (userPreferencesProvider.shouldShowHiddenEvents()) { - TimelineSettings(30, false, true, TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES) + TimelineSettings(30, false, true, TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES, userPreferencesProvider.shouldShowReadReceipts()) } else { - TimelineSettings(30, true, true, TimelineDisplayableEvents.DISPLAYABLE_TYPES) + TimelineSettings(30, true, true, TimelineDisplayableEvents.DISPLAYABLE_TYPES, userPreferencesProvider.shouldShowReadReceipts()) } private var timeline = room.createTimeline(eventId, timelineSettings)