Read marker: start working on it (no UI)

This commit is contained in:
ganfra 2019-08-20 18:30:24 +02:00
parent 8ca829d538
commit d8f449388c
23 changed files with 304 additions and 56 deletions

View File

@ -16,7 +16,7 @@ org.gradle.jvmargs=-Xmx1536m




vector.debugPrivateData=false 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 # 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 #vector.debugPrivateData=true

View File

@ -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
)

View File

@ -42,5 +42,10 @@ interface ReadService {


fun isEventRead(eventId: String): Boolean fun isEventRead(eventId: String): Boolean


/**
* Returns a nullable read marker for the room.
*/
fun getReadMarkerLive(): LiveData<String?>

fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>> fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>>
} }

View File

@ -39,7 +39,8 @@ data class TimelineEvent(
val isUniqueDisplayName: Boolean, val isUniqueDisplayName: Boolean,
val senderAvatar: String?, val senderAvatar: String?,
val annotations: EventAnnotationsSummary? = null, val annotations: EventAnnotationsSummary? = null,
val readReceipts: List<ReadReceipt> = emptyList() val readReceipts: List<ReadReceipt> = emptyList(),
val hasReadMarker: Boolean = false
) { ) {


val metadata = HashMap<String, Any>() val metadata = HashMap<String, Any>()

View File

@ -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.mapper.toEntity
import im.vector.matrix.android.internal.database.model.ChunkEntity 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.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.ReadReceiptEntity
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.TimelineEventEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity
@ -157,7 +158,6 @@ internal fun ChunkEntity.add(roomId: String,
} }
} }



val eventEntity = TimelineEventEntity(localId).also { val eventEntity = TimelineEventEntity(localId).also {
it.root = event.toEntity(roomId).apply { it.root = event.toEntity(roomId).apply {
this.stateIndex = currentStateIndex this.stateIndex = currentStateIndex
@ -169,6 +169,7 @@ internal fun ChunkEntity.add(roomId: String,
it.roomId = roomId it.roomId = roomId
it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
it.readReceipts = readReceiptsSummaryEntity it.readReceipts = readReceiptsSummaryEntity
it.readMarker = ReadMarkerEntity.where(realm, roomId = roomId, eventId = eventId).findFirst()
} }
val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size
timelineEvents.add(position, eventEntity) timelineEvents.add(position, eventEntity)

View File

@ -26,8 +26,8 @@ import java.util.*
import javax.inject.Inject import javax.inject.Inject


internal class RoomSummaryMapper @Inject constructor( internal class RoomSummaryMapper @Inject constructor(
val cryptoService: CryptoService, private val cryptoService: CryptoService,
val timelineEventMapper: TimelineEventMapper private val timelineEventMapper: TimelineEventMapper
) { ) {


fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary { fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary {

View File

@ -45,7 +45,8 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS
senderAvatar = timelineEventEntity.senderAvatar, senderAvatar = timelineEventEntity.senderAvatar,
readReceipts = readReceipts?.sortedByDescending { readReceipts = readReceipts?.sortedByDescending {
it.originServerTs it.originServerTs
} ?: emptyList() } ?: emptyList(),
hasReadMarker = timelineEventEntity.readMarker?.eventId?.isEmpty() == false
) )
} }



View File

@ -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<TimelineEventEntity>? = null

companion object

}

View File

@ -43,6 +43,7 @@ import io.realm.annotations.RealmModule
PushConditionEntity::class, PushConditionEntity::class,
PusherEntity::class, PusherEntity::class,
PusherDataEntity::class, PusherDataEntity::class,
ReadReceiptsSummaryEntity::class ReadReceiptsSummaryEntity::class,
ReadMarkerEntity::class
]) ])
internal class SessionRealmModule internal class SessionRealmModule

View File

@ -31,7 +31,8 @@ internal open class TimelineEventEntity(var localId: Long = 0,
var isUniqueDisplayName: Boolean = false, var isUniqueDisplayName: Boolean = false,
var senderAvatar: String? = null, var senderAvatar: String? = null,
var senderMembershipEvent: EventEntity? = null, var senderMembershipEvent: EventEntity? = null,
var readReceipts: ReadReceiptsSummaryEntity? = null var readReceipts: ReadReceiptsSummaryEntity? = null,
var readMarker: ReadMarkerEntity? = null
) : RealmObject() { ) : RealmObject() {


@LinkingObjects("timelineEvents") @LinkingObjects("timelineEvents")

View File

@ -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<ReadMarkerEntity> {
val query = realm.where<ReadMarkerEntity>()
.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)
}

View File

@ -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.RealmLiveData
import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper 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.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.ReadReceiptEntity
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.RoomSummaryEntity
import im.vector.matrix.android.internal.database.query.find 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.findLastLiveChunkFromRoom
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
@ -93,6 +95,15 @@ internal class DefaultReadService @AssistedInject constructor(@Assisted private
return isEventRead return isEventRead
} }


override fun getReadMarkerLive(): LiveData<String?> {
val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm ->
ReadMarkerEntity.where(realm, roomId)
}
return Transformations.map(liveRealmData) { results ->
results.firstOrNull()?.eventId
}
}

override fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>> { override fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>> {
val liveEntity = RealmLiveData(monarchy.realmConfiguration) { realm -> val liveEntity = RealmLiveData(monarchy.realmConfiguration) { realm ->
ReadReceiptsSummaryEntity.where(realm, eventId) ReadReceiptsSummaryEntity.where(realm, eventId)

View File

@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.session.room.read
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.auth.data.Credentials 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.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.ReadReceiptEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity 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 fullyReadEventId: String?
val readReceiptEventId: String? val readReceiptEventId: String?


Timber.v("Execute set read marker with params: $params")
if (params.markAllAsRead) { if (params.markAllAsRead) {
val latestSyncedEventId = Realm.getInstance(monarchy.realmConfiguration).use { realm -> val latestSyncedEventId = Realm.getInstance(monarchy.realmConfiguration).use { realm ->
TimelineEventEntity.latestEvent(realm, roomId = params.roomId, includesSending = false)?.eventId TimelineEventEntity.latestEvent(realm, roomId = params.roomId, includesSending = false)?.eventId
@ -68,7 +70,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
readReceiptEventId = params.readReceiptEventId readReceiptEventId = params.readReceiptEventId
} }


if (fullyReadEventId != null) { if (fullyReadEventId != null && isReadMarkerMoreRecent(params.roomId, fullyReadEventId)) {
if (LocalEchoEventFactory.isLocalEchoId(fullyReadEventId)) { if (LocalEchoEventFactory.isLocalEchoId(fullyReadEventId)) {
Timber.w("Can't set read marker for local event ${params.fullyReadEventId}") Timber.w("Can't set read marker for local event ${params.fullyReadEventId}")
} else { } else {
@ -76,7 +78,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
} }
} }
if (readReceiptEventId != null if (readReceiptEventId != null
&& !isEventRead(params.roomId, readReceiptEventId)) { && !isEventRead(params.roomId, readReceiptEventId)) {


if (LocalEchoEventFactory.isLocalEchoId(readReceiptEventId)) { if (LocalEchoEventFactory.isLocalEchoId(readReceiptEventId)) {
Timber.w("Can't set read receipt for local event ${params.fullyReadEventId}") 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) { private fun updateNotificationCountIfNecessary(roomId: String, eventId: String) {
monarchy.writeAsync { realm -> monarchy.writeAsync { realm ->
val isLatestReceived = TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId == eventId val isLatestReceived = TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId == eventId
if (isLatestReceived) { if (isLatestReceived) {
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
?: return@writeAsync ?: return@writeAsync
roomSummary.notificationCount = 0 roomSummary.notificationCount = 0
roomSummary.highlightCount = 0 roomSummary.highlightCount = 0
} }
@ -106,19 +119,17 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
} }


private fun isEventRead(roomId: String, eventId: String): Boolean { private fun isEventRead(roomId: String, eventId: String): Boolean {
var isEventRead = false return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
monarchy.doWithRealm { val readReceipt = ReadReceiptEntity.where(realm, roomId, credentials.userId).findFirst()
val readReceipt = ReadReceiptEntity.where(it, roomId, credentials.userId).findFirst() ?: return false
?: return@doWithRealm val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(it, roomId) ?: return false
?: return@doWithRealm
val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex
?: Int.MIN_VALUE ?: Int.MIN_VALUE
val eventToCheckIndex = liveChunk.timelineEvents.find(eventId)?.root?.displayIndex val eventToCheckIndex = liveChunk.timelineEvents.find(eventId)?.root?.displayIndex
?: Int.MAX_VALUE ?: Int.MAX_VALUE
isEventRead = eventToCheckIndex <= readReceiptIndex eventToCheckIndex <= readReceiptIndex
} }
return isEventRead
} }


} }

View File

@ -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.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.ReadMarkerEntity
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
@ -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.Debouncer
import im.vector.matrix.android.internal.util.createBackgroundHandler import im.vector.matrix.android.internal.util.createBackgroundHandler
import im.vector.matrix.android.internal.util.createUIHandler import im.vector.matrix.android.internal.util.createUIHandler
import io.realm.ObjectChangeSet
import io.realm.OrderedCollectionChangeSet import io.realm.OrderedCollectionChangeSet
import io.realm.OrderedRealmCollectionChangeListener import io.realm.OrderedRealmCollectionChangeListener
import io.realm.Realm import io.realm.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import io.realm.RealmObjectChangeListener
import io.realm.RealmQuery import io.realm.RealmQuery
import io.realm.RealmResults import io.realm.RealmResults
import io.realm.Sort import io.realm.Sort
@ -101,6 +104,7 @@ internal class DefaultTimeline(
private lateinit var eventRelations: RealmResults<EventAnnotationsSummaryEntity> private lateinit var eventRelations: RealmResults<EventAnnotationsSummaryEntity>


private var roomEntity: RoomEntity? = null private var roomEntity: RoomEntity? = null
private var readMarkerEntity: ReadMarkerEntity? = null


private var prevDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN private var prevDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN
private var nextDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN private var nextDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN
@ -149,13 +153,9 @@ internal class DefaultTimeline(
changeSet.changes.forEach { index -> changeSet.changes.forEach { index ->
val eventEntity = results[index] val eventEntity = results[index]
eventEntity?.eventId?.let { eventId -> eventEntity?.eventId?.let { eventId ->
builtEventsIdMap[eventId]?.let { builtIndex -> hasChanged = rebuildEvent(eventId) {
//Update an existing event buildTimelineEvent(eventEntity)
builtEvents[builtIndex]?.let { te -> } || hasChanged
builtEvents[builtIndex] = buildTimelineEvent(eventEntity)
hasChanged = true
}
}
} }
} }
if (hasChanged) postSnapshot() if (hasChanged) postSnapshot()
@ -163,27 +163,44 @@ internal class DefaultTimeline(
} }


private val relationsListener = OrderedRealmCollectionChangeListener<RealmResults<EventAnnotationsSummaryEntity>> { collection, changeSet -> private val relationsListener = OrderedRealmCollectionChangeListener<RealmResults<EventAnnotationsSummaryEntity>> { collection, changeSet ->

var hasChange = false var hasChange = false


(changeSet.insertions + changeSet.changes).forEach { (changeSet.insertions + changeSet.changes).forEach {
val eventRelations = collection[it] val eventRelations = collection[it]
if (eventRelations != null) { if (eventRelations != null) {
builtEventsIdMap[eventRelations.eventId]?.let { builtIndex -> hasChange = rebuildEvent(eventRelations.eventId) { te ->
//Update the relation of existing event te.copy(annotations = eventRelations.asDomain())
builtEvents[builtIndex]?.let { te -> } || hasChange
builtEvents[builtIndex] = te.copy(annotations = eventRelations.asDomain()) }
hasChange = true }
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) if (hasChange) postSnapshot()
postSnapshot()
} }




// Public methods ****************************************************************************** // Public methods ******************************************************************************


override fun paginate(direction: Timeline.Direction, count: Int) { override fun paginate(direction: Timeline.Direction, count: Int) {
BACKGROUND_HANDLER.post { BACKGROUND_HANDLER.post {
@ -237,6 +254,10 @@ internal class DefaultTimeline(
.findAllAsync() .findAllAsync()
.also { it.addChangeListener(relationsListener) } .also { it.addChangeListener(relationsListener) }


readMarkerEntity = ReadMarkerEntity.where(realm, roomId = roomId)
.findFirstAsync()
.also { it.addChangeListener(readMarkerListener) }

if (settings.buildReadReceipts) { if (settings.buildReadReceipts) {
hiddenReadReceipts.start(realm, liveEvents, this) hiddenReadReceipts.start(realm, liveEvents, this)
} }
@ -255,6 +276,7 @@ internal class DefaultTimeline(
roomEntity?.sendingTimelineEvents?.removeAllChangeListeners() roomEntity?.sendingTimelineEvents?.removeAllChangeListeners()
eventRelations.removeAllChangeListeners() eventRelations.removeAllChangeListeners()
liveEvents.removeAllChangeListeners() liveEvents.removeAllChangeListeners()
readMarkerEntity?.removeAllChangeListeners()
if (settings.buildReadReceipts) { if (settings.buildReadReceipts) {
hiddenReadReceipts.dispose() hiddenReadReceipts.dispose()
} }
@ -272,20 +294,26 @@ internal class DefaultTimeline(
// TimelineHiddenReadReceipts.Delegate // TimelineHiddenReadReceipts.Delegate


override fun rebuildEvent(eventId: String, readReceipts: List<ReadReceipt>): Boolean { override fun rebuildEvent(eventId: String, readReceipts: List<ReadReceipt>): Boolean {
return builtEventsIdMap[eventId]?.let { builtIndex -> return rebuildEvent(eventId) { te ->
//Update the relation of existing event te.copy(readReceipts = readReceipts)
builtEvents[builtIndex]?.let { te -> }
builtEvents[builtIndex] = te.copy(readReceipts = readReceipts)
true
}
} ?: false
} }


override fun onReadReceiptsUpdated() { override fun onReadReceiptsUpdated() {
postSnapshot() 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 { private fun hasMoreInCache(direction: Timeline.Direction): Boolean {
return Realm.getInstance(realmConfiguration).use { localRealm -> return Realm.getInstance(realmConfiguration).use { localRealm ->
@ -571,7 +599,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

View File

@ -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
}

}

View File

@ -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.events.model.toModel
import im.vector.matrix.android.api.session.room.model.Membership 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.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.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.ChunkEntity
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.RoomEntity 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.notification.ProcessEventForPushTask
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater 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.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.session.user.UserEntityFactory
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith 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 readReceiptHandler: ReadReceiptHandler,
private val roomSummaryUpdater: RoomSummaryUpdater, private val roomSummaryUpdater: RoomSummaryUpdater,
private val roomTagHandler: RoomTagHandler, private val roomTagHandler: RoomTagHandler,
private val roomFullyReadHandler: RoomFullyReadHandler,
private val cryptoManager: CryptoManager, private val cryptoManager: CryptoManager,
private val tokenStore: SyncTokenStore, private val tokenStore: SyncTokenStore,
private val pushRuleService: DefaultPushRuleService, 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) { private fun handleRoomAccountDataEvents(realm: Realm, roomId: String, accountData: RoomSyncAccountData) {
accountData.events for (event in accountData.events) {
.asSequence() val eventType = event.getClearType()
.filter { it.getClearType() == EventType.TAG } if (eventType == EventType.TAG) {
.map { it.content.toModel<RoomTagContent>() } val content = event.getClearContent().toModel<RoomTagContent>()
.forEach { roomTagHandler.handle(realm, roomId, it) } roomTagHandler.handle(realm, roomId, content)
} else if (eventType == EventType.FULLY_READ) {
val content = event.getClearContent().toModel<FullyReadContent>()
roomFullyReadHandler.handle(realm, roomId, content)
}
}
} }


} }

View File

@ -626,9 +626,13 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
.buffer(1, TimeUnit.SECONDS) .buffer(1, TimeUnit.SECONDS)
.filter { it.isNotEmpty() } .filter { it.isNotEmpty() }
.subscribeBy(onNext = { actions -> .subscribeBy(onNext = { actions ->
val readMarkerVisible = actions.find { it.event.hasReadMarker } != null
val mostRecentEvent = actions.maxBy { it.event.displayIndex } val mostRecentEvent = actions.maxBy { it.event.displayIndex }
mostRecentEvent?.event?.root?.eventId?.let { eventId -> mostRecentEvent?.event?.root?.eventId?.let { eventId ->
room.setReadReceipt(eventId, callback = object : MatrixCallback<Unit> {}) room.setReadReceipt(eventId, callback = object : MatrixCallback<Unit> {})
if (readMarkerVisible) {
room.setReadMarker(eventId, callback = object : MatrixCallback<Unit> {})
}
} }
}) })
.disposeOnClear() .disposeOnClear()

View File

@ -129,6 +129,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
holder.memberNameView.setOnLongClickListener(null) holder.memberNameView.setOnLongClickListener(null)
} }


holder.readMarkerView.isVisible = informationData.displayReadMarker
holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener) holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener)


if (!shouldShowReactionAtBottom() || informationData.orderedReactionList.isNullOrEmpty()) { if (!shouldShowReactionAtBottom() || informationData.orderedReactionList.isNullOrEmpty()) {
@ -182,6 +183,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
val memberNameView by bind<TextView>(R.id.messageMemberNameView) val memberNameView by bind<TextView>(R.id.messageMemberNameView)
val timeView by bind<TextView>(R.id.messageTimeView) val timeView by bind<TextView>(R.id.messageTimeView)
val readReceiptsView by bind<ReadReceiptsView>(R.id.readReceiptsView) val readReceiptsView by bind<ReadReceiptsView>(R.id.readReceiptsView)
val readMarkerView by bind<View>(R.id.readMarkerView)
var reactionWrapper: ViewGroup? = null var reactionWrapper: ViewGroup? = null
var reactionFlowHelper: Flow? = null var reactionFlowHelper: Flow? = null
} }

View File

@ -33,7 +33,8 @@ data class MessageInformationData(
val orderedReactionList: List<ReactionInfoData>? = null, val orderedReactionList: List<ReactionInfoData>? = null,
val hasBeenEdited: Boolean = false, val hasBeenEdited: Boolean = false,
val hasPendingEdits: Boolean = false, val hasPendingEdits: Boolean = false,
val readReceipts: List<ReadReceiptData> = emptyList() val readReceipts: List<ReadReceiptData> = emptyList(),
val displayReadMarker: Boolean = false
) : Parcelable ) : Parcelable





View File

@ -19,6 +19,7 @@ package im.vector.riotx.features.home.room.detail.timeline.item
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R import im.vector.riotx.R
@ -65,6 +66,7 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
) )
holder.view.setOnLongClickListener(longClickListener) holder.view.setOnLongClickListener(longClickListener)
holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener) holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener)
holder.readMarkerView.isVisible = informationData.displayReadMarker
} }


override fun getViewType() = STUB_ID override fun getViewType() = STUB_ID
@ -73,6 +75,7 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
val avatarImageView by bind<ImageView>(R.id.itemNoticeAvatarView) val avatarImageView by bind<ImageView>(R.id.itemNoticeAvatarView)
val noticeTextView by bind<TextView>(R.id.itemNoticeTextView) val noticeTextView by bind<TextView>(R.id.itemNoticeTextView)
val readReceiptsView by bind<ReadReceiptsView>(R.id.readReceiptsView) val readReceiptsView by bind<ReadReceiptsView>(R.id.readReceiptsView)
val readMarkerView by bind<View>(R.id.readMarkerView)
} }


companion object { companion object {

View File

@ -62,6 +62,8 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId ?: "")) textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId ?: ""))
} }


val displayReadMarker = event.hasReadMarker && event.readReceipts.find { it.user.userId == session.myUserId } == null

return MessageInformationData( return MessageInformationData(
eventId = eventId, eventId = eventId,
senderId = event.root.senderId ?: "", senderId = event.root.senderId ?: "",
@ -85,7 +87,8 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
.map { .map {
ReadReceiptData(it.user.userId, it.user.avatarUrl, it.user.displayName, it.originServerTs) ReadReceiptData(it.user.userId, it.user.avatarUrl, it.user.displayName, it.originServerTs)
} }
.toList() .toList(),
displayReadMarker = displayReadMarker
) )
} }
} }

View File

@ -122,7 +122,6 @@


</ViewStub> </ViewStub>



<im.vector.riotx.core.ui.views.ReadReceiptsView <im.vector.riotx.core.ui.views.ReadReceiptsView
android:id="@+id/readReceiptsView" android:id="@+id/readReceiptsView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -132,5 +131,14 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" /> app:layout_constraintEnd_toEndOf="parent" />


<View
android:id="@+id/readMarkerView"
android:layout_width="0dp"
android:layout_height="4dp"
android:background="@android:color/holo_green_light"
app:layout_constraintEnd_toEndOf="parent"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>


</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -61,5 +61,15 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" /> app:layout_constraintEnd_toEndOf="parent" />


<View
android:id="@+id/readMarkerView"
android:layout_width="0dp"
android:layout_height="4dp"
android:background="@android:color/holo_green_light"
app:layout_constraintEnd_toEndOf="parent"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>



</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>