mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-24 18:36:21 +03:00
Read marker: start working on it (no UI)
This commit is contained in:
parent
8ca829d538
commit
d8f449388c
23 changed files with 304 additions and 56 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -42,5 +42,10 @@ interface ReadService {
|
|||
|
||||
fun isEventRead(eventId: String): Boolean
|
||||
|
||||
/**
|
||||
* Returns a nullable read marker for the room.
|
||||
*/
|
||||
fun getReadMarkerLive(): LiveData<String?>
|
||||
|
||||
fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>>
|
||||
}
|
|
@ -39,7 +39,8 @@ data class TimelineEvent(
|
|||
val isUniqueDisplayName: Boolean,
|
||||
val senderAvatar: String?,
|
||||
val annotations: EventAnnotationsSummary? = null,
|
||||
val readReceipts: List<ReadReceipt> = emptyList()
|
||||
val readReceipts: List<ReadReceipt> = emptyList(),
|
||||
val hasReadMarker: Boolean = false
|
||||
) {
|
||||
|
||||
val metadata = HashMap<String, Any>()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
|
@ -43,6 +43,7 @@ import io.realm.annotations.RealmModule
|
|||
PushConditionEntity::class,
|
||||
PusherEntity::class,
|
||||
PusherDataEntity::class,
|
||||
ReadReceiptsSummaryEntity::class
|
||||
ReadReceiptsSummaryEntity::class,
|
||||
ReadMarkerEntity::class
|
||||
])
|
||||
internal class SessionRealmModule
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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<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>> {
|
||||
val liveEntity = RealmLiveData(monarchy.realmConfiguration) { realm ->
|
||||
ReadReceiptsSummaryEntity.where(realm, eventId)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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<EventAnnotationsSummaryEntity>
|
||||
|
||||
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<RealmResults<EventAnnotationsSummaryEntity>> { 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<ReadReceipt>): Boolean {
|
||||
return builtEventsIdMap[eventId]?.let { builtIndex ->
|
||||
//Update the relation of existing event
|
||||
builtEvents[builtIndex]?.let { te ->
|
||||
builtEvents[builtIndex] = te.copy(readReceipts = readReceipts)
|
||||
true
|
||||
}
|
||||
} ?: false
|
||||
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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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<RoomTagContent>() }
|
||||
.forEach { roomTagHandler.handle(realm, roomId, it) }
|
||||
for (event in accountData.events) {
|
||||
val eventType = event.getClearType()
|
||||
if (eventType == EventType.TAG) {
|
||||
val content = event.getClearContent().toModel<RoomTagContent>()
|
||||
roomTagHandler.handle(realm, roomId, content)
|
||||
} else if (eventType == EventType.FULLY_READ) {
|
||||
val content = event.getClearContent().toModel<FullyReadContent>()
|
||||
roomFullyReadHandler.handle(realm, roomId, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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<Unit> {})
|
||||
if (readMarkerVisible) {
|
||||
room.setReadMarker(eventId, callback = object : MatrixCallback<Unit> {})
|
||||
}
|
||||
}
|
||||
})
|
||||
.disposeOnClear()
|
||||
|
|
|
@ -129,6 +129,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
|||
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<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
|||
val memberNameView by bind<TextView>(R.id.messageMemberNameView)
|
||||
val timeView by bind<TextView>(R.id.messageTimeView)
|
||||
val readReceiptsView by bind<ReadReceiptsView>(R.id.readReceiptsView)
|
||||
val readMarkerView by bind<View>(R.id.readMarkerView)
|
||||
var reactionWrapper: ViewGroup? = null
|
||||
var reactionFlowHelper: Flow? = null
|
||||
}
|
||||
|
|
|
@ -33,7 +33,8 @@ data class MessageInformationData(
|
|||
val orderedReactionList: List<ReactionInfoData>? = null,
|
||||
val hasBeenEdited: Boolean = false,
|
||||
val hasPendingEdits: Boolean = false,
|
||||
val readReceipts: List<ReadReceiptData> = emptyList()
|
||||
val readReceipts: List<ReadReceiptData> = emptyList(),
|
||||
val displayReadMarker: Boolean = false
|
||||
) : Parcelable
|
||||
|
||||
|
||||
|
|
|
@ -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<NoticeItem.Holder>() {
|
|||
)
|
||||
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<NoticeItem.Holder>() {
|
|||
val avatarImageView by bind<ImageView>(R.id.itemNoticeAvatarView)
|
||||
val noticeTextView by bind<TextView>(R.id.itemNoticeTextView)
|
||||
val readReceiptsView by bind<ReadReceiptsView>(R.id.readReceiptsView)
|
||||
val readMarkerView by bind<View>(R.id.readMarkerView)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -122,7 +122,6 @@
|
|||
|
||||
</ViewStub>
|
||||
|
||||
|
||||
<im.vector.riotx.core.ui.views.ReadReceiptsView
|
||||
android:id="@+id/readReceiptsView"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -132,5 +131,14 @@
|
|||
app:layout_constraintBottom_toBottomOf="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>
|
|
@ -61,5 +61,15 @@
|
|||
app:layout_constraintBottom_toBottomOf="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>
|
Loading…
Reference in a new issue