From d8f449388ca3e70bf3ba25e8486d7e04b12fb41e Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 20 Aug 2019 18:30:24 +0200 Subject: [PATCH] Read marker: start working on it (no UI) --- gradle.properties | 2 +- .../api/session/room/read/FullyReadContent.kt | 25 ++++++ .../api/session/room/read/ReadService.kt | 5 ++ .../session/room/timeline/TimelineEvent.kt | 3 +- .../database/helper/ChunkEntityHelper.kt | 3 +- .../database/mapper/RoomSummaryMapper.kt | 4 +- .../database/mapper/TimelineEventMapper.kt | 3 +- .../database/model/ReadMarkerEntity.kt | 35 +++++++++ .../database/model/SessionRealmModule.kt | 3 +- .../database/model/TimelineEventEntity.kt | 3 +- .../database/query/ReadMarkerEntityQueries.kt | 37 +++++++++ .../session/room/read/DefaultReadService.kt | 11 +++ .../session/room/read/SetReadMarkersTask.kt | 37 +++++---- .../session/room/timeline/DefaultTimeline.kt | 78 +++++++++++++------ .../session/sync/RoomFullyReadHandler.kt | 45 +++++++++++ .../internal/session/sync/RoomSyncHandler.kt | 29 +++++-- .../home/room/detail/RoomDetailViewModel.kt | 4 + .../detail/timeline/item/AbsMessageItem.kt | 2 + .../timeline/item/MessageInformationData.kt | 3 +- .../room/detail/timeline/item/NoticeItem.kt | 3 + .../util/MessageInformationDataFactory.kt | 5 +- .../res/layout/item_timeline_event_base.xml | 10 ++- .../item_timeline_event_base_noinfo.xml | 10 +++ 23 files changed, 304 insertions(+), 56 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/FullyReadContent.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadMarkerEntity.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadMarkerEntityQueries.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt diff --git a/gradle.properties b/gradle.properties index 2e2b110f15..35ca815df8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,7 +16,7 @@ org.gradle.jvmargs=-Xmx1536m vector.debugPrivateData=false -vector.httpLogLevel=NONE +vector.httpLogLevel=HEADERS # Note: to debug, you can put and uncomment the following lines in the file ~/.gradle/gradle.properties to override the value above #vector.debugPrivateData=true diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/FullyReadContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/FullyReadContent.kt new file mode 100644 index 0000000000..a73b9ef5b7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/FullyReadContent.kt @@ -0,0 +1,25 @@ +/* + * 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.api.session.room.read + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class FullyReadContent( + @Json(name = "event_id") val eventId: String +) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt index d97fc497f0..0ff0298b44 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt @@ -42,5 +42,10 @@ interface ReadService { fun isEventRead(eventId: String): Boolean + /** + * Returns a nullable read marker for the room. + */ + fun getReadMarkerLive(): LiveData + fun getEventReadReceiptsLive(eventId: String): LiveData> } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt index 36ca360e08..f250824b1f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt @@ -39,7 +39,8 @@ data class TimelineEvent( val isUniqueDisplayName: Boolean, val senderAvatar: String?, val annotations: EventAnnotationsSummary? = null, - val readReceipts: List = emptyList() + val readReceipts: List = emptyList(), + val hasReadMarker: Boolean = false ) { val metadata = HashMap() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt index 69065f5171..3824fed779 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt @@ -23,6 +23,7 @@ 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.EventAnnotationsSummaryEntity +import im.vector.matrix.android.internal.database.model.ReadMarkerEntity 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 @@ -157,7 +158,6 @@ internal fun ChunkEntity.add(roomId: String, } } - val eventEntity = TimelineEventEntity(localId).also { it.root = event.toEntity(roomId).apply { this.stateIndex = currentStateIndex @@ -169,6 +169,7 @@ internal fun ChunkEntity.add(roomId: String, it.roomId = roomId it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() it.readReceipts = readReceiptsSummaryEntity + it.readMarker = ReadMarkerEntity.where(realm, roomId = roomId, eventId = eventId).findFirst() } val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size timelineEvents.add(position, eventEntity) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt index 95d4d8bc62..03061c6edd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt @@ -26,8 +26,8 @@ import java.util.* import javax.inject.Inject internal class RoomSummaryMapper @Inject constructor( - val cryptoService: CryptoService, - val timelineEventMapper: TimelineEventMapper + private val cryptoService: CryptoService, + private val timelineEventMapper: TimelineEventMapper ) { fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary { 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 fe98ebfb5b..0e9f13155e 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 @@ -45,7 +45,8 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS senderAvatar = timelineEventEntity.senderAvatar, readReceipts = readReceipts?.sortedByDescending { it.originServerTs - } ?: emptyList() + } ?: emptyList(), + hasReadMarker = timelineEventEntity.readMarker?.eventId?.isEmpty() == false ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadMarkerEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadMarkerEntity.kt new file mode 100644 index 0000000000..d67308b283 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadMarkerEntity.kt @@ -0,0 +1,35 @@ +/* + * 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.model + +import io.realm.RealmObject +import io.realm.RealmResults +import io.realm.annotations.LinkingObjects +import io.realm.annotations.PrimaryKey + +internal open class ReadMarkerEntity( + @PrimaryKey + var roomId: String = "", + var eventId: String = "" +) : RealmObject() { + + @LinkingObjects("readMarker") + val timelineEvent: RealmResults? = null + + companion object + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt index 1d27bf07ee..0aa6ac1dd6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt @@ -43,6 +43,7 @@ import io.realm.annotations.RealmModule PushConditionEntity::class, PusherEntity::class, PusherDataEntity::class, - ReadReceiptsSummaryEntity::class + ReadReceiptsSummaryEntity::class, + ReadMarkerEntity::class ]) internal class SessionRealmModule diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt index 429b2291f6..e727ce40c7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt @@ -31,7 +31,8 @@ internal open class TimelineEventEntity(var localId: Long = 0, var isUniqueDisplayName: Boolean = false, var senderAvatar: String? = null, var senderMembershipEvent: EventEntity? = null, - var readReceipts: ReadReceiptsSummaryEntity? = null + var readReceipts: ReadReceiptsSummaryEntity? = null, + var readMarker: ReadMarkerEntity? = null ) : RealmObject() { @LinkingObjects("timelineEvents") diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadMarkerEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadMarkerEntityQueries.kt new file mode 100644 index 0000000000..061634a9da --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadMarkerEntityQueries.kt @@ -0,0 +1,37 @@ +/* + * 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 + +import im.vector.matrix.android.internal.database.model.ReadMarkerEntity +import im.vector.matrix.android.internal.database.model.ReadMarkerEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where + +internal fun ReadMarkerEntity.Companion.where(realm: Realm, roomId: String, eventId: String? = null): RealmQuery { + val query = realm.where() + .equalTo(ReadMarkerEntityFields.ROOM_ID, roomId) + if (eventId != null) { + query.equalTo(ReadMarkerEntityFields.EVENT_ID, eventId) + } + return query +} + +internal fun ReadMarkerEntity.Companion.getOrCreate(realm: Realm, roomId: String): ReadMarkerEntity { + return where(realm, roomId).findFirst() + ?: realm.createObject(ReadMarkerEntity::class.java, roomId) +} 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 505b958911..3709521cc3 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 @@ -28,8 +28,10 @@ import im.vector.matrix.android.api.session.room.read.ReadService import im.vector.matrix.android.internal.database.RealmLiveData import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper import im.vector.matrix.android.internal.database.model.ChunkEntity +import im.vector.matrix.android.internal.database.model.ReadMarkerEntity 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.RoomSummaryEntity import im.vector.matrix.android.internal.database.query.find import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom import im.vector.matrix.android.internal.database.query.where @@ -93,6 +95,15 @@ internal class DefaultReadService @AssistedInject constructor(@Assisted private return isEventRead } + override fun getReadMarkerLive(): LiveData { + val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm -> + ReadMarkerEntity.where(realm, roomId) + } + return Transformations.map(liveRealmData) { results -> + results.firstOrNull()?.eventId + } + } + override fun getEventReadReceiptsLive(eventId: String): LiveData> { val liveEntity = RealmLiveData(monarchy.realmConfiguration) { realm -> ReadReceiptsSummaryEntity.where(realm, eventId) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt index 41c9cca507..af05510c8a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt @@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.session.room.read import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.internal.database.model.ChunkEntity +import im.vector.matrix.android.internal.database.model.ReadMarkerEntity import im.vector.matrix.android.internal.database.model.ReadReceiptEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity @@ -57,6 +58,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI val fullyReadEventId: String? val readReceiptEventId: String? + Timber.v("Execute set read marker with params: $params") if (params.markAllAsRead) { val latestSyncedEventId = Realm.getInstance(monarchy.realmConfiguration).use { realm -> TimelineEventEntity.latestEvent(realm, roomId = params.roomId, includesSending = false)?.eventId @@ -68,7 +70,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI readReceiptEventId = params.readReceiptEventId } - if (fullyReadEventId != null) { + if (fullyReadEventId != null && isReadMarkerMoreRecent(params.roomId, fullyReadEventId)) { if (LocalEchoEventFactory.isLocalEchoId(fullyReadEventId)) { Timber.w("Can't set read marker for local event ${params.fullyReadEventId}") } else { @@ -76,7 +78,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI } } if (readReceiptEventId != null - && !isEventRead(params.roomId, readReceiptEventId)) { + && !isEventRead(params.roomId, readReceiptEventId)) { if (LocalEchoEventFactory.isLocalEchoId(readReceiptEventId)) { Timber.w("Can't set read receipt for local event ${params.fullyReadEventId}") @@ -93,12 +95,23 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI } } + private fun isReadMarkerMoreRecent(roomId: String, fullyReadEventId: String): Boolean { + return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + val readMarkerEntity = ReadMarkerEntity.where(realm, roomId = roomId).findFirst() + val readMarkerEvent = readMarkerEntity?.timelineEvent?.firstOrNull() + val eventToCheck = TimelineEventEntity.where(realm, eventId = fullyReadEventId).findFirst() + val readReceiptIndex = readMarkerEvent?.root?.displayIndex ?: Int.MAX_VALUE + val eventToCheckIndex = eventToCheck?.root?.displayIndex ?: Int.MIN_VALUE + eventToCheckIndex > readReceiptIndex + } + } + private fun updateNotificationCountIfNecessary(roomId: String, eventId: String) { monarchy.writeAsync { realm -> val isLatestReceived = TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId == eventId if (isLatestReceived) { val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() - ?: return@writeAsync + ?: return@writeAsync roomSummary.notificationCount = 0 roomSummary.highlightCount = 0 } @@ -106,19 +119,17 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI } private fun isEventRead(roomId: String, eventId: String): Boolean { - var isEventRead = false - monarchy.doWithRealm { - val readReceipt = ReadReceiptEntity.where(it, roomId, credentials.userId).findFirst() - ?: return@doWithRealm - val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(it, roomId) - ?: return@doWithRealm + return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + val readReceipt = ReadReceiptEntity.where(realm, roomId, credentials.userId).findFirst() + ?: return false + val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) + ?: return false val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex - ?: Int.MIN_VALUE + ?: Int.MIN_VALUE val eventToCheckIndex = liveChunk.timelineEvents.find(eventId)?.root?.displayIndex - ?: Int.MAX_VALUE - isEventRead = eventToCheckIndex <= readReceiptIndex + ?: Int.MAX_VALUE + eventToCheckIndex <= readReceiptIndex } - return isEventRead } } \ 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 03f5da6e6f..26983a82c6 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 @@ -32,6 +32,7 @@ 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.ReadMarkerEntity 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 @@ -47,10 +48,12 @@ import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.Debouncer import im.vector.matrix.android.internal.util.createBackgroundHandler import im.vector.matrix.android.internal.util.createUIHandler +import io.realm.ObjectChangeSet import io.realm.OrderedCollectionChangeSet import io.realm.OrderedRealmCollectionChangeListener import io.realm.Realm import io.realm.RealmConfiguration +import io.realm.RealmObjectChangeListener import io.realm.RealmQuery import io.realm.RealmResults import io.realm.Sort @@ -101,6 +104,7 @@ internal class DefaultTimeline( private lateinit var eventRelations: RealmResults private var roomEntity: RoomEntity? = null + private var readMarkerEntity: ReadMarkerEntity? = null private var prevDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN private var nextDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN @@ -149,13 +153,9 @@ internal class DefaultTimeline( changeSet.changes.forEach { index -> val eventEntity = results[index] eventEntity?.eventId?.let { eventId -> - builtEventsIdMap[eventId]?.let { builtIndex -> - //Update an existing event - builtEvents[builtIndex]?.let { te -> - builtEvents[builtIndex] = buildTimelineEvent(eventEntity) - hasChanged = true - } - } + hasChanged = rebuildEvent(eventId) { + buildTimelineEvent(eventEntity) + } || hasChanged } } if (hasChanged) postSnapshot() @@ -163,27 +163,44 @@ internal class DefaultTimeline( } private val relationsListener = OrderedRealmCollectionChangeListener> { collection, changeSet -> - var hasChange = false (changeSet.insertions + changeSet.changes).forEach { val eventRelations = collection[it] if (eventRelations != null) { - builtEventsIdMap[eventRelations.eventId]?.let { builtIndex -> - //Update the relation of existing event - builtEvents[builtIndex]?.let { te -> - builtEvents[builtIndex] = te.copy(annotations = eventRelations.asDomain()) - hasChange = true + hasChange = rebuildEvent(eventRelations.eventId) { te -> + te.copy(annotations = eventRelations.asDomain()) + } || hasChange + } + } + if (hasChange) postSnapshot() + } + + private val readMarkerListener = RealmObjectChangeListener { readMarkerEntity: ReadMarkerEntity, _: ObjectChangeSet? -> + val isEventHidden = liveEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, readMarkerEntity.eventId).findFirst() == null + var hasChange = false + if (isEventHidden) { + val hiddenEvent = readMarkerEntity.timelineEvent?.firstOrNull() ?: return@RealmObjectChangeListener + val displayIndex = hiddenEvent.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 rebuild this one with marker + if (firstDisplayedEvent != null) { + hasChange = rebuildEvent(firstDisplayedEvent.eventId) { + it.copy(hasReadMarker = true) } } } } - if (hasChange) - postSnapshot() + if (hasChange) postSnapshot() } - // Public methods ****************************************************************************** +// Public methods ****************************************************************************** override fun paginate(direction: Timeline.Direction, count: Int) { BACKGROUND_HANDLER.post { @@ -237,6 +254,10 @@ internal class DefaultTimeline( .findAllAsync() .also { it.addChangeListener(relationsListener) } + readMarkerEntity = ReadMarkerEntity.where(realm, roomId = roomId) + .findFirstAsync() + .also { it.addChangeListener(readMarkerListener) } + if (settings.buildReadReceipts) { hiddenReadReceipts.start(realm, liveEvents, this) } @@ -255,6 +276,7 @@ internal class DefaultTimeline( roomEntity?.sendingTimelineEvents?.removeAllChangeListeners() eventRelations.removeAllChangeListeners() liveEvents.removeAllChangeListeners() + readMarkerEntity?.removeAllChangeListeners() if (settings.buildReadReceipts) { hiddenReadReceipts.dispose() } @@ -272,20 +294,26 @@ internal class DefaultTimeline( // 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 + return rebuildEvent(eventId) { te -> + te.copy(readReceipts = readReceipts) + } } override fun onReadReceiptsUpdated() { postSnapshot() } -// Private methods ***************************************************************************** + // Private methods ***************************************************************************** + + private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent): Boolean { + return builtEventsIdMap[eventId]?.let { builtIndex -> + //Update the relation of existing event + builtEvents[builtIndex]?.let { te -> + builtEvents[builtIndex] = builder(te) + true + } + } ?: false + } private fun hasMoreInCache(direction: Timeline.Direction): Boolean { return Realm.getInstance(realmConfiguration).use { localRealm -> @@ -571,7 +599,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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt new file mode 100644 index 0000000000..f142ca069e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt @@ -0,0 +1,45 @@ +/* + * 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.sync + +import im.vector.matrix.android.api.session.room.read.FullyReadContent +import im.vector.matrix.android.internal.database.model.ReadMarkerEntity +import im.vector.matrix.android.internal.database.model.TimelineEventEntity +import im.vector.matrix.android.internal.database.query.getOrCreate +import im.vector.matrix.android.internal.database.query.where +import io.realm.Realm +import timber.log.Timber +import javax.inject.Inject + +internal class RoomFullyReadHandler @Inject constructor() { + + fun handle(realm: Realm, roomId: String, content: FullyReadContent?) { + if (content == null) { + return + } + Timber.v("Handle for roomId: $roomId eventId: ${content.eventId}") + val readMarkerEntity = ReadMarkerEntity.getOrCreate(realm, roomId).apply { + eventId = content.eventId + } + // Remove the old marker if any + readMarkerEntity.timelineEvent?.firstOrNull()?.readMarker = null + // Attach to timelineEvent if known + val timelineEventEntity = TimelineEventEntity.where(realm, eventId = content.eventId).findFirst() + timelineEventEntity?.readMarker = readMarkerEntity + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt index 74b56e774c..0b4897e089 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt @@ -23,8 +23,13 @@ import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.tag.RoomTagContent +import im.vector.matrix.android.api.session.room.read.FullyReadContent import im.vector.matrix.android.internal.crypto.CryptoManager -import im.vector.matrix.android.internal.database.helper.* +import im.vector.matrix.android.internal.database.helper.add +import im.vector.matrix.android.internal.database.helper.addOrUpdate +import im.vector.matrix.android.internal.database.helper.addStateEvent +import im.vector.matrix.android.internal.database.helper.lastStateIndex +import im.vector.matrix.android.internal.database.helper.updateSenderDataFor import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.EventEntityFields import im.vector.matrix.android.internal.database.model.RoomEntity @@ -37,7 +42,11 @@ import im.vector.matrix.android.internal.session.notification.DefaultPushRuleSer import im.vector.matrix.android.internal.session.notification.ProcessEventForPushTask import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection -import im.vector.matrix.android.internal.session.sync.model.* +import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync +import im.vector.matrix.android.internal.session.sync.model.RoomSync +import im.vector.matrix.android.internal.session.sync.model.RoomSyncAccountData +import im.vector.matrix.android.internal.session.sync.model.RoomSyncEphemeral +import im.vector.matrix.android.internal.session.sync.model.RoomsSyncResponse import im.vector.matrix.android.internal.session.user.UserEntityFactory import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith @@ -50,6 +59,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch private val readReceiptHandler: ReadReceiptHandler, private val roomSummaryUpdater: RoomSummaryUpdater, private val roomTagHandler: RoomTagHandler, + private val roomFullyReadHandler: RoomFullyReadHandler, private val cryptoManager: CryptoManager, private val tokenStore: SyncTokenStore, private val pushRuleService: DefaultPushRuleService, @@ -247,11 +257,16 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch } private fun handleRoomAccountDataEvents(realm: Realm, roomId: String, accountData: RoomSyncAccountData) { - accountData.events - .asSequence() - .filter { it.getClearType() == EventType.TAG } - .map { it.content.toModel() } - .forEach { roomTagHandler.handle(realm, roomId, it) } + for (event in accountData.events) { + val eventType = event.getClearType() + if (eventType == EventType.TAG) { + val content = event.getClearContent().toModel() + roomTagHandler.handle(realm, roomId, content) + } else if (eventType == EventType.FULLY_READ) { + val content = event.getClearContent().toModel() + roomFullyReadHandler.handle(realm, roomId, content) + } + } } } 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 1cd8cc4a41..607f999e30 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 @@ -626,9 +626,13 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro .buffer(1, TimeUnit.SECONDS) .filter { it.isNotEmpty() } .subscribeBy(onNext = { actions -> + val readMarkerVisible = actions.find { it.event.hasReadMarker } != null val mostRecentEvent = actions.maxBy { it.event.displayIndex } mostRecentEvent?.event?.root?.eventId?.let { eventId -> room.setReadReceipt(eventId, callback = object : MatrixCallback {}) + if (readMarkerVisible) { + room.setReadMarker(eventId, callback = object : MatrixCallback {}) + } } }) .disposeOnClear() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt index a394f47124..570daf669c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -129,6 +129,7 @@ abstract class AbsMessageItem : BaseEventItem() { holder.memberNameView.setOnLongClickListener(null) } + holder.readMarkerView.isVisible = informationData.displayReadMarker holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener) if (!shouldShowReactionAtBottom() || informationData.orderedReactionList.isNullOrEmpty()) { @@ -182,6 +183,7 @@ abstract class AbsMessageItem : BaseEventItem() { val memberNameView by bind(R.id.messageMemberNameView) val timeView by bind(R.id.messageTimeView) val readReceiptsView by bind(R.id.readReceiptsView) + val readMarkerView by bind(R.id.readMarkerView) var reactionWrapper: ViewGroup? = null var reactionFlowHelper: Flow? = null } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt index d46b2a8db3..041b6dbddd 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt @@ -33,7 +33,8 @@ data class MessageInformationData( val orderedReactionList: List? = null, val hasBeenEdited: Boolean = false, val hasPendingEdits: Boolean = false, - val readReceipts: List = emptyList() + val readReceipts: List = emptyList(), + val displayReadMarker: Boolean = false ) : Parcelable diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt index 51a7b0ce38..dd42dc7b66 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt @@ -19,6 +19,7 @@ package im.vector.riotx.features.home.room.detail.timeline.item import android.view.View import android.widget.ImageView import android.widget.TextView +import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R @@ -65,6 +66,7 @@ abstract class NoticeItem : BaseEventItem() { ) holder.view.setOnLongClickListener(longClickListener) holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener) + holder.readMarkerView.isVisible = informationData.displayReadMarker } override fun getViewType() = STUB_ID @@ -73,6 +75,7 @@ abstract class NoticeItem : BaseEventItem() { val avatarImageView by bind(R.id.itemNoticeAvatarView) val noticeTextView by bind(R.id.itemNoticeTextView) val readReceiptsView by bind(R.id.readReceiptsView) + val readMarkerView by bind(R.id.readMarkerView) } companion object { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/util/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/util/MessageInformationDataFactory.kt index a00dd3fa9f..71a7549b46 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/util/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/util/MessageInformationDataFactory.kt @@ -62,6 +62,8 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId ?: "")) } + val displayReadMarker = event.hasReadMarker && event.readReceipts.find { it.user.userId == session.myUserId } == null + return MessageInformationData( eventId = eventId, senderId = event.root.senderId ?: "", @@ -85,7 +87,8 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses .map { ReadReceiptData(it.user.userId, it.user.avatarUrl, it.user.displayName, it.originServerTs) } - .toList() + .toList(), + displayReadMarker = displayReadMarker ) } } \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml index 2f0be78f38..ea4cfd5d4a 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -122,7 +122,6 @@ - + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml index 7726839902..ad6999c5ee 100644 --- a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml +++ b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml @@ -61,5 +61,15 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" /> + + \ No newline at end of file