added read receipts for threads (#7474)

This commit is contained in:
Nikita Fedrunov 2022-11-25 09:49:06 +01:00 committed by GitHub
parent 27419f0d33
commit 18bcc83a46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 214 additions and 93 deletions

1
changelog.d/6996.sdk Normal file
View File

@ -0,0 +1 @@
Added support for read receipts in threads. Now user in a room can have multiple read receipts (one per thread + one in main thread + one without threadId)

View File

@ -100,8 +100,8 @@ class FlowRoom(private val room: Room) {
return room.readService().getReadMarkerLive().asFlow() return room.readService().getReadMarkerLive().asFlow()
} }
fun liveReadReceipt(): Flow<Optional<String>> { fun liveReadReceipt(threadId: String?): Flow<Optional<String>> {
return room.readService().getMyReadReceiptLive().asFlow() return room.readService().getMyReadReceiptLive(threadId).asFlow()
} }
fun liveEventReadReceipts(eventId: String): Flow<List<ReadReceipt>> { fun liveEventReadReceipts(eventId: String): Flow<List<ReadReceipt>> {

View File

@ -18,5 +18,6 @@ package org.matrix.android.sdk.api.session.room.model
data class ReadReceipt( data class ReadReceipt(
val roomMember: RoomMemberSummary, val roomMember: RoomMemberSummary,
val originServerTs: Long val originServerTs: Long,
val threadId: String?
) )

View File

@ -34,12 +34,14 @@ interface ReadService {
/** /**
* Force the read marker to be set on the latest event. * Force the read marker to be set on the latest event.
*/ */
suspend fun markAsRead(params: MarkAsReadParams = MarkAsReadParams.BOTH) suspend fun markAsRead(params: MarkAsReadParams = MarkAsReadParams.BOTH, mainTimeLineOnly: Boolean = true)
/** /**
* Set the read receipt on the event with provided eventId. * Set the read receipt on the event with provided eventId.
* @param eventId the id of the event where read receipt will be set
* @param threadId the id of the thread in which read receipt will be set. For main thread use [ReadService.THREAD_ID_MAIN] constant
*/ */
suspend fun setReadReceipt(eventId: String) suspend fun setReadReceipt(eventId: String, threadId: String)
/** /**
* Set the read marker on the event with provided eventId. * Set the read marker on the event with provided eventId.
@ -59,10 +61,10 @@ interface ReadService {
/** /**
* Returns a live read receipt id for the room. * Returns a live read receipt id for the room.
*/ */
fun getMyReadReceiptLive(): LiveData<Optional<String>> fun getMyReadReceiptLive(threadId: String?): LiveData<Optional<String>>
/** /**
* Get the eventId where the read receipt for the provided user is. * Get the eventId from the main timeline where the read receipt for the provided user is.
* @param userId the id of the user to look for * @param userId the id of the user to look for
* *
* @return the eventId where the read receipt for the provided user is attached, or null if not found * @return the eventId where the read receipt for the provided user is attached, or null if not found
@ -74,4 +76,8 @@ interface ReadService {
* @param eventId the event * @param eventId the event
*/ */
fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>> fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>>
companion object {
const val THREAD_ID_MAIN = "main"
}
} }

View File

@ -132,7 +132,7 @@ private fun handleReadReceipts(realm: Realm, roomId: String, eventEntity: EventE
val originServerTs = eventEntity.originServerTs val originServerTs = eventEntity.originServerTs
if (originServerTs != null) { if (originServerTs != null) {
val timestampOfEvent = originServerTs.toDouble() val timestampOfEvent = originServerTs.toDouble()
val readReceiptOfSender = ReadReceiptEntity.getOrCreate(realm, roomId = roomId, userId = senderId) val readReceiptOfSender = ReadReceiptEntity.getOrCreate(realm, roomId = roomId, userId = senderId, threadId = eventEntity.rootThreadEventId)
// If the synced RR is older, update // If the synced RR is older, update
if (timestampOfEvent > readReceiptOfSender.originServerTs) { if (timestampOfEvent > readReceiptOfSender.originServerTs) {
val previousReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId = readReceiptOfSender.eventId).findFirst() val previousReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId = readReceiptOfSender.eventId).findFirst()

View File

@ -65,11 +65,11 @@ internal fun Map<String, EventEntity>.updateThreadSummaryIfNeeded(
inThreadMessages = inThreadMessages, inThreadMessages = inThreadMessages,
latestMessageTimelineEventEntity = latestEventInThread latestMessageTimelineEventEntity = latestEventInThread
) )
}
}
if (shouldUpdateNotifications) { if (shouldUpdateNotifications) {
updateNotificationsNew(roomId, realm, currentUserId) updateThreadNotifications(roomId, realm, currentUserId, rootThreadEventId)
}
}
} }
} }
@ -273,8 +273,8 @@ internal fun TimelineEventEntity.Companion.isUserMentionedInThread(realm: Realm,
/** /**
* Find the read receipt for the current user. * Find the read receipt for the current user.
*/ */
internal fun findMyReadReceipt(realm: Realm, roomId: String, userId: String): String? = internal fun findMyReadReceipt(realm: Realm, roomId: String, userId: String, threadId: String?): String? =
ReadReceiptEntity.where(realm, roomId = roomId, userId = userId) ReadReceiptEntity.where(realm, roomId = roomId, userId = userId, threadId = threadId)
.findFirst() .findFirst()
?.eventId ?.eventId
@ -293,28 +293,29 @@ internal fun isUserMentioned(currentUserId: String, timelineEventEntity: Timelin
* Important: It will work only with the latest chunk, while read marker will be changed * Important: It will work only with the latest chunk, while read marker will be changed
* immediately so we should not display wrong notifications * immediately so we should not display wrong notifications
*/ */
internal fun updateNotificationsNew(roomId: String, realm: Realm, currentUserId: String) { internal fun updateThreadNotifications(roomId: String, realm: Realm, currentUserId: String, rootThreadEventId: String) {
val readReceipt = findMyReadReceipt(realm, roomId, currentUserId) ?: return val readReceipt = findMyReadReceipt(realm, roomId, currentUserId, threadId = rootThreadEventId) ?: return
val readReceiptChunk = ChunkEntity val readReceiptChunk = ChunkEntity
.findIncludingEvent(realm, readReceipt) ?: return .findIncludingEvent(realm, readReceipt) ?: return
val readReceiptChunkTimelineEvents = readReceiptChunk val readReceiptChunkThreadEvents = readReceiptChunk
.timelineEvents .timelineEvents
.where() .where()
.equalTo(TimelineEventEntityFields.ROOM_ID, roomId) .equalTo(TimelineEventEntityFields.ROOM_ID, roomId)
.equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId)
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING) .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
.findAll() ?: return .findAll() ?: return
val readReceiptChunkPosition = readReceiptChunkTimelineEvents.indexOfFirst { it.eventId == readReceipt } val readReceiptChunkPosition = readReceiptChunkThreadEvents.indexOfFirst { it.eventId == readReceipt }
if (readReceiptChunkPosition == -1) return if (readReceiptChunkPosition == -1) return
if (readReceiptChunkPosition < readReceiptChunkTimelineEvents.lastIndex) { if (readReceiptChunkPosition < readReceiptChunkThreadEvents.lastIndex) {
// If the read receipt is found inside the chunk // If the read receipt is found inside the chunk
val threadEventsAfterReadReceipt = readReceiptChunkTimelineEvents val threadEventsAfterReadReceipt = readReceiptChunkThreadEvents
.slice(readReceiptChunkPosition..readReceiptChunkTimelineEvents.lastIndex) .slice(readReceiptChunkPosition..readReceiptChunkThreadEvents.lastIndex)
.filter { it.root?.isThread() == true } .filter { it.root?.isThread() == true }
// In order for the below code to work for old events, we should save the previous read receipt // In order for the below code to work for old events, we should save the previous read receipt
@ -343,26 +344,21 @@ internal fun updateNotificationsNew(roomId: String, realm: Realm, currentUserId:
it.root?.rootThreadEventId it.root?.rootThreadEventId
} }
// Find the root events in the new thread events // Update root thread event only if the user have participated in
val rootThreads = threadEventsAfterReadReceipt.distinctBy { it.root?.rootThreadEventId }.mapNotNull { it.root?.rootThreadEventId }
// Update root thread events only if the user have participated in
rootThreads.forEach { eventId ->
val isUserParticipating = TimelineEventEntity.isUserParticipatingInThread( val isUserParticipating = TimelineEventEntity.isUserParticipatingInThread(
realm = realm, realm = realm,
roomId = roomId, roomId = roomId,
rootThreadEventId = eventId, rootThreadEventId = rootThreadEventId,
senderId = currentUserId senderId = currentUserId
) )
val rootThreadEventEntity = EventEntity.where(realm, eventId).findFirst() val rootThreadEventEntity = EventEntity.where(realm, rootThreadEventId).findFirst()
if (isUserParticipating) { if (isUserParticipating) {
rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_MESSAGE rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_MESSAGE
} }
if (userMentionsList.contains(eventId)) { if (userMentionsList.contains(rootThreadEventId)) {
rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE
} }
} }
} }
}

View File

@ -50,7 +50,7 @@ internal class ReadReceiptsSummaryMapper @Inject constructor(
.mapNotNull { .mapNotNull {
val roomMember = RoomMemberSummaryEntity.where(realm, roomId = it.roomId, userId = it.userId).findFirst() val roomMember = RoomMemberSummaryEntity.where(realm, roomId = it.roomId, userId = it.userId).findFirst()
?: return@mapNotNull null ?: return@mapNotNull null
ReadReceipt(roomMember.asDomain(), it.originServerTs.toLong()) ReadReceipt(roomMember.asDomain(), it.originServerTs.toLong(), it.threadId)
} }
} }
} }

View File

@ -26,6 +26,7 @@ internal open class ReadReceiptEntity(
var eventId: String = "", var eventId: String = "",
var roomId: String = "", var roomId: String = "",
var userId: String = "", var userId: String = "",
var threadId: String? = null,
var originServerTs: Double = 0.0 var originServerTs: Double = 0.0
) : RealmObject() { ) : RealmObject() {
companion object companion object

View File

@ -20,6 +20,7 @@ import io.realm.RealmObject
import io.realm.RealmResults import io.realm.RealmResults
import io.realm.annotations.Index import io.realm.annotations.Index
import io.realm.annotations.LinkingObjects import io.realm.annotations.LinkingObjects
import org.matrix.android.sdk.api.session.room.read.ReadService
import org.matrix.android.sdk.internal.extensions.assertIsManaged import org.matrix.android.sdk.internal.extensions.assertIsManaged
internal open class TimelineEventEntity( internal open class TimelineEventEntity(
@ -52,3 +53,7 @@ internal fun TimelineEventEntity.deleteOnCascade(canDeleteRoot: Boolean) {
} }
deleteFromRealm() deleteFromRealm()
} }
internal fun TimelineEventEntity.getThreadId(): String {
return root?.rootThreadEventId ?: ReadService.THREAD_ID_MAIN
}

View File

@ -18,17 +18,20 @@ package org.matrix.android.sdk.internal.database.query
import io.realm.Realm import io.realm.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import org.matrix.android.sdk.api.session.events.model.LocalEcho import org.matrix.android.sdk.api.session.events.model.LocalEcho
import org.matrix.android.sdk.api.session.room.read.ReadService
import org.matrix.android.sdk.internal.database.helper.isMoreRecentThan import org.matrix.android.sdk.internal.database.helper.isMoreRecentThan
import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.ChunkEntity
import org.matrix.android.sdk.internal.database.model.ReadMarkerEntity import org.matrix.android.sdk.internal.database.model.ReadMarkerEntity
import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.model.getThreadId
internal fun isEventRead( internal fun isEventRead(
realmConfiguration: RealmConfiguration, realmConfiguration: RealmConfiguration,
userId: String?, userId: String?,
roomId: String?, roomId: String?,
eventId: String? eventId: String?,
shouldCheckIfReadInEventsThread: Boolean
): Boolean { ): Boolean {
if (userId.isNullOrBlank() || roomId.isNullOrBlank() || eventId.isNullOrBlank()) { if (userId.isNullOrBlank() || roomId.isNullOrBlank() || eventId.isNullOrBlank()) {
return false return false
@ -45,7 +48,8 @@ internal fun isEventRead(
eventToCheck.root?.sender == userId -> true eventToCheck.root?.sender == userId -> true
// If new event exists and the latest event is from ourselves we can infer the event is read // If new event exists and the latest event is from ourselves we can infer the event is read
latestEventIsFromSelf(realm, roomId, userId) -> true latestEventIsFromSelf(realm, roomId, userId) -> true
eventToCheck.isBeforeLatestReadReceipt(realm, roomId, userId) -> true eventToCheck.isBeforeLatestReadReceipt(realm, roomId, userId, null) -> true
(shouldCheckIfReadInEventsThread && eventToCheck.isBeforeLatestReadReceipt(realm, roomId, userId, eventToCheck.getThreadId())) -> true
else -> false else -> false
} }
} }
@ -54,27 +58,33 @@ internal fun isEventRead(
private fun latestEventIsFromSelf(realm: Realm, roomId: String, userId: String) = TimelineEventEntity.latestEvent(realm, roomId, true) private fun latestEventIsFromSelf(realm: Realm, roomId: String, userId: String) = TimelineEventEntity.latestEvent(realm, roomId, true)
?.root?.sender == userId ?.root?.sender == userId
private fun TimelineEventEntity.isBeforeLatestReadReceipt(realm: Realm, roomId: String, userId: String): Boolean { private fun TimelineEventEntity.isBeforeLatestReadReceipt(realm: Realm, roomId: String, userId: String, threadId: String?): Boolean {
return ReadReceiptEntity.where(realm, roomId, userId).findFirst()?.let { readReceipt -> val isMoreRecent = ReadReceiptEntity.where(realm, roomId, userId, threadId).findFirst()?.let { readReceipt ->
val readReceiptEvent = TimelineEventEntity.where(realm, roomId, readReceipt.eventId).findFirst() val readReceiptEvent = TimelineEventEntity.where(realm, roomId, readReceipt.eventId).findFirst()
readReceiptEvent?.isMoreRecentThan(this) readReceiptEvent?.isMoreRecentThan(this)
} ?: false } ?: false
return isMoreRecent
} }
/** /**
* Missing events can be caused by the latest timeline chunk no longer contain an older event or * Missing events can be caused by the latest timeline chunk no longer contain an older event or
* by fast lane eagerly displaying events before the database has finished updating. * by fast lane eagerly displaying events before the database has finished updating.
*/ */
private fun hasReadMissingEvent(realm: Realm, latestChunkEntity: ChunkEntity, roomId: String, userId: String, eventId: String): Boolean { private fun hasReadMissingEvent(realm: Realm,
return realm.doesEventExistInChunkHistory(eventId) && realm.hasReadReceiptInLatestChunk(latestChunkEntity, roomId, userId) latestChunkEntity: ChunkEntity,
roomId: String,
userId: String,
eventId: String,
threadId: String? = ReadService.THREAD_ID_MAIN): Boolean {
return realm.doesEventExistInChunkHistory(eventId) && realm.hasReadReceiptInLatestChunk(latestChunkEntity, roomId, userId, threadId)
} }
private fun Realm.doesEventExistInChunkHistory(eventId: String): Boolean { private fun Realm.doesEventExistInChunkHistory(eventId: String): Boolean {
return ChunkEntity.findIncludingEvent(this, eventId) != null return ChunkEntity.findIncludingEvent(this, eventId) != null
} }
private fun Realm.hasReadReceiptInLatestChunk(latestChunkEntity: ChunkEntity, roomId: String, userId: String): Boolean { private fun Realm.hasReadReceiptInLatestChunk(latestChunkEntity: ChunkEntity, roomId: String, userId: String, threadId: String?): Boolean {
return ReadReceiptEntity.where(this, roomId = roomId, userId = userId).findFirst()?.let { return ReadReceiptEntity.where(this, roomId = roomId, userId = userId, threadId = threadId).findFirst()?.let {
latestChunkEntity.timelineEvents.find(it.eventId) latestChunkEntity.timelineEvents.find(it.eventId)
} != null } != null
} }

View File

@ -20,12 +20,20 @@ import io.realm.Realm
import io.realm.RealmQuery import io.realm.RealmQuery
import io.realm.kotlin.createObject import io.realm.kotlin.createObject
import io.realm.kotlin.where import io.realm.kotlin.where
import org.matrix.android.sdk.api.session.room.read.ReadService
import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity
import org.matrix.android.sdk.internal.database.model.ReadReceiptEntityFields import org.matrix.android.sdk.internal.database.model.ReadReceiptEntityFields
internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, userId: String): RealmQuery<ReadReceiptEntity> { internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, userId: String, threadId: String?): RealmQuery<ReadReceiptEntity> {
return realm.where<ReadReceiptEntity>() return realm.where<ReadReceiptEntity>()
.equalTo(ReadReceiptEntityFields.PRIMARY_KEY, buildPrimaryKey(roomId, userId)) .equalTo(ReadReceiptEntityFields.PRIMARY_KEY, buildPrimaryKey(roomId, userId, threadId))
}
internal fun ReadReceiptEntity.Companion.forMainTimelineWhere(realm: Realm, roomId: String, userId: String): RealmQuery<ReadReceiptEntity> {
return realm.where<ReadReceiptEntity>()
.equalTo(ReadReceiptEntityFields.PRIMARY_KEY, buildPrimaryKey(roomId, userId, ReadService.THREAD_ID_MAIN))
.or()
.equalTo(ReadReceiptEntityFields.PRIMARY_KEY, buildPrimaryKey(roomId, userId, null))
} }
internal fun ReadReceiptEntity.Companion.whereUserId(realm: Realm, userId: String): RealmQuery<ReadReceiptEntity> { internal fun ReadReceiptEntity.Companion.whereUserId(realm: Realm, userId: String): RealmQuery<ReadReceiptEntity> {
@ -38,23 +46,37 @@ internal fun ReadReceiptEntity.Companion.whereRoomId(realm: Realm, roomId: Strin
.equalTo(ReadReceiptEntityFields.ROOM_ID, roomId) .equalTo(ReadReceiptEntityFields.ROOM_ID, roomId)
} }
internal fun ReadReceiptEntity.Companion.createUnmanaged(roomId: String, eventId: String, userId: String, originServerTs: Double): ReadReceiptEntity { internal fun ReadReceiptEntity.Companion.createUnmanaged(
roomId: String,
eventId: String,
userId: String,
threadId: String?,
originServerTs: Double
): ReadReceiptEntity {
return ReadReceiptEntity().apply { return ReadReceiptEntity().apply {
this.primaryKey = "${roomId}_$userId" this.primaryKey = buildPrimaryKey(roomId, userId, threadId)
this.eventId = eventId this.eventId = eventId
this.roomId = roomId this.roomId = roomId
this.userId = userId this.userId = userId
this.threadId = threadId
this.originServerTs = originServerTs this.originServerTs = originServerTs
} }
} }
internal fun ReadReceiptEntity.Companion.getOrCreate(realm: Realm, roomId: String, userId: String): ReadReceiptEntity { internal fun ReadReceiptEntity.Companion.getOrCreate(realm: Realm, roomId: String, userId: String, threadId: String?): ReadReceiptEntity {
return ReadReceiptEntity.where(realm, roomId, userId).findFirst() return ReadReceiptEntity.where(realm, roomId, userId, threadId).findFirst()
?: realm.createObject<ReadReceiptEntity>(buildPrimaryKey(roomId, userId)) ?: realm.createObject<ReadReceiptEntity>(buildPrimaryKey(roomId, userId, threadId))
.apply { .apply {
this.roomId = roomId this.roomId = roomId
this.userId = userId this.userId = userId
this.threadId = threadId
} }
} }
private fun buildPrimaryKey(roomId: String, userId: String) = "${roomId}_$userId" private fun buildPrimaryKey(roomId: String, userId: String, threadId: String?): String {
return if (threadId == null) {
"${roomId}_${userId}"
} else {
"${roomId}_${userId}_${threadId}"
}
}

View File

@ -33,6 +33,7 @@ import org.matrix.android.sdk.internal.session.room.membership.RoomMembersRespon
import org.matrix.android.sdk.internal.session.room.membership.admin.UserIdAndReason import org.matrix.android.sdk.internal.session.room.membership.admin.UserIdAndReason
import org.matrix.android.sdk.internal.session.room.membership.joining.InviteBody import org.matrix.android.sdk.internal.session.room.membership.joining.InviteBody
import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody
import org.matrix.android.sdk.internal.session.room.read.ReadBody
import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse
import org.matrix.android.sdk.internal.session.room.reporting.ReportContentBody import org.matrix.android.sdk.internal.session.room.reporting.ReportContentBody
import org.matrix.android.sdk.internal.session.room.send.SendResponse import org.matrix.android.sdk.internal.session.room.send.SendResponse
@ -173,7 +174,7 @@ internal interface RoomAPI {
@Path("roomId") roomId: String, @Path("roomId") roomId: String,
@Path("receiptType") receiptType: String, @Path("receiptType") receiptType: String,
@Path("eventId") eventId: String, @Path("eventId") eventId: String,
@Body body: JsonDict = emptyMap() @Body body: ReadBody
) )
/** /**

View File

@ -30,17 +30,20 @@ import org.matrix.android.sdk.internal.database.mapper.ReadReceiptsSummaryMapper
import org.matrix.android.sdk.internal.database.model.ReadMarkerEntity import org.matrix.android.sdk.internal.database.model.ReadMarkerEntity
import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity
import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity
import org.matrix.android.sdk.internal.database.query.forMainTimelineWhere
import org.matrix.android.sdk.internal.database.query.isEventRead import org.matrix.android.sdk.internal.database.query.isEventRead
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource
internal class DefaultReadService @AssistedInject constructor( internal class DefaultReadService @AssistedInject constructor(
@Assisted private val roomId: String, @Assisted private val roomId: String,
@SessionDatabase private val monarchy: Monarchy, @SessionDatabase private val monarchy: Monarchy,
private val setReadMarkersTask: SetReadMarkersTask, private val setReadMarkersTask: SetReadMarkersTask,
private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper,
@UserId private val userId: String @UserId private val userId: String,
private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource
) : ReadService { ) : ReadService {
@AssistedFactory @AssistedFactory
@ -48,17 +51,28 @@ internal class DefaultReadService @AssistedInject constructor(
fun create(roomId: String): DefaultReadService fun create(roomId: String): DefaultReadService
} }
override suspend fun markAsRead(params: ReadService.MarkAsReadParams) { override suspend fun markAsRead(params: ReadService.MarkAsReadParams, mainTimeLineOnly: Boolean) {
val readReceiptThreadId = if (homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.canUseThreadReadReceiptsAndNotifications == true) {
if (mainTimeLineOnly) ReadService.THREAD_ID_MAIN else null
} else {
null
}
val taskParams = SetReadMarkersTask.Params( val taskParams = SetReadMarkersTask.Params(
roomId = roomId, roomId = roomId,
forceReadMarker = params.forceReadMarker(), forceReadMarker = params.forceReadMarker(),
forceReadReceipt = params.forceReadReceipt() forceReadReceipt = params.forceReadReceipt(),
readReceiptThreadId = readReceiptThreadId
) )
setReadMarkersTask.execute(taskParams) setReadMarkersTask.execute(taskParams)
} }
override suspend fun setReadReceipt(eventId: String) { override suspend fun setReadReceipt(eventId: String, threadId: String) {
val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = null, readReceiptEventId = eventId) val readReceiptThreadId = if (homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.canUseThreadReadReceiptsAndNotifications == true) {
threadId
} else {
null
}
val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = null, readReceiptEventId = eventId, readReceiptThreadId = readReceiptThreadId)
setReadMarkersTask.execute(params) setReadMarkersTask.execute(params)
} }
@ -68,7 +82,8 @@ internal class DefaultReadService @AssistedInject constructor(
} }
override fun isEventRead(eventId: String): Boolean { override fun isEventRead(eventId: String): Boolean {
return isEventRead(monarchy.realmConfiguration, userId, roomId, eventId) val shouldCheckIfReadInEventsThread = homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.canUseThreadReadReceiptsAndNotifications == true
return isEventRead(monarchy.realmConfiguration, userId, roomId, eventId, shouldCheckIfReadInEventsThread)
} }
override fun getReadMarkerLive(): LiveData<Optional<String>> { override fun getReadMarkerLive(): LiveData<Optional<String>> {
@ -81,9 +96,9 @@ internal class DefaultReadService @AssistedInject constructor(
} }
} }
override fun getMyReadReceiptLive(): LiveData<Optional<String>> { override fun getMyReadReceiptLive(threadId: String?): LiveData<Optional<String>> {
val liveRealmData = monarchy.findAllMappedWithChanges( val liveRealmData = monarchy.findAllMappedWithChanges(
{ ReadReceiptEntity.where(it, roomId = roomId, userId = userId) }, { ReadReceiptEntity.where(it, roomId = roomId, userId = userId, threadId = threadId) },
{ it.eventId } { it.eventId }
) )
return Transformations.map(liveRealmData) { return Transformations.map(liveRealmData) {
@ -94,10 +109,11 @@ internal class DefaultReadService @AssistedInject constructor(
override fun getUserReadReceipt(userId: String): String? { override fun getUserReadReceipt(userId: String): String? {
var eventId: String? = null var eventId: String? = null
monarchy.doWithRealm { monarchy.doWithRealm {
eventId = ReadReceiptEntity.where(it, roomId = roomId, userId = userId) eventId = ReadReceiptEntity.forMainTimelineWhere(it, roomId = roomId, userId = userId)
.findFirst() .findFirst()
?.eventId ?.eventId
} }
return eventId return eventId
} }

View File

@ -0,0 +1,25 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.room.read
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class ReadBody(
@Json(name = "thread_id") val threadId: String?,
)

View File

@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.room.read
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import io.realm.Realm import io.realm.Realm
import org.matrix.android.sdk.api.session.events.model.LocalEcho import org.matrix.android.sdk.api.session.events.model.LocalEcho
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.query.isEventRead import org.matrix.android.sdk.internal.database.query.isEventRead
@ -45,8 +46,9 @@ internal interface SetReadMarkersTask : Task<SetReadMarkersTask.Params, Unit> {
val roomId: String, val roomId: String,
val fullyReadEventId: String? = null, val fullyReadEventId: String? = null,
val readReceiptEventId: String? = null, val readReceiptEventId: String? = null,
val readReceiptThreadId: String? = null,
val forceReadReceipt: Boolean = false, val forceReadReceipt: Boolean = false,
val forceReadMarker: Boolean = false val forceReadMarker: Boolean = false,
) )
} }
@ -61,12 +63,14 @@ internal class DefaultSetReadMarkersTask @Inject constructor(
@UserId private val userId: String, @UserId private val userId: String,
private val globalErrorReceiver: GlobalErrorReceiver, private val globalErrorReceiver: GlobalErrorReceiver,
private val clock: Clock, private val clock: Clock,
private val homeServerCapabilitiesService: HomeServerCapabilitiesService,
) : SetReadMarkersTask { ) : SetReadMarkersTask {
override suspend fun execute(params: SetReadMarkersTask.Params) { override suspend fun execute(params: SetReadMarkersTask.Params) {
val markers = mutableMapOf<String, String>() val markers = mutableMapOf<String, String>()
Timber.v("Execute set read marker with params: $params") Timber.v("Execute set read marker with params: $params")
val latestSyncedEventId = latestSyncedEventId(params.roomId) val latestSyncedEventId = latestSyncedEventId(params.roomId)
val readReceiptThreadId = params.readReceiptThreadId
val fullyReadEventId = if (params.forceReadMarker) { val fullyReadEventId = if (params.forceReadMarker) {
latestSyncedEventId latestSyncedEventId
} else { } else {
@ -77,6 +81,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(
} else { } else {
params.readReceiptEventId params.readReceiptEventId
} }
if (fullyReadEventId != null && !isReadMarkerMoreRecent(monarchy.realmConfiguration, params.roomId, fullyReadEventId)) { if (fullyReadEventId != null && !isReadMarkerMoreRecent(monarchy.realmConfiguration, params.roomId, fullyReadEventId)) {
if (LocalEcho.isLocalEchoId(fullyReadEventId)) { if (LocalEcho.isLocalEchoId(fullyReadEventId)) {
Timber.w("Can't set read marker for local event $fullyReadEventId") Timber.w("Can't set read marker for local event $fullyReadEventId")
@ -84,8 +89,12 @@ internal class DefaultSetReadMarkersTask @Inject constructor(
markers[READ_MARKER] = fullyReadEventId markers[READ_MARKER] = fullyReadEventId
} }
} }
val shouldCheckIfReadInEventsThread = readReceiptThreadId != null &&
homeServerCapabilitiesService.getHomeServerCapabilities().canUseThreadReadReceiptsAndNotifications
if (readReceiptEventId != null && if (readReceiptEventId != null &&
!isEventRead(monarchy.realmConfiguration, userId, params.roomId, readReceiptEventId)) { !isEventRead(monarchy.realmConfiguration, userId, params.roomId, readReceiptEventId, shouldCheckIfReadInEventsThread)) {
if (LocalEcho.isLocalEchoId(readReceiptEventId)) { if (LocalEcho.isLocalEchoId(readReceiptEventId)) {
Timber.w("Can't set read receipt for local event $readReceiptEventId") Timber.w("Can't set read receipt for local event $readReceiptEventId")
} else { } else {
@ -95,7 +104,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(
val shouldUpdateRoomSummary = readReceiptEventId != null && readReceiptEventId == latestSyncedEventId val shouldUpdateRoomSummary = readReceiptEventId != null && readReceiptEventId == latestSyncedEventId
if (markers.isNotEmpty() || shouldUpdateRoomSummary) { if (markers.isNotEmpty() || shouldUpdateRoomSummary) {
updateDatabase(params.roomId, markers, shouldUpdateRoomSummary) updateDatabase(params.roomId, readReceiptThreadId, markers, shouldUpdateRoomSummary)
} }
if (markers.isNotEmpty()) { if (markers.isNotEmpty()) {
executeRequest( executeRequest(
@ -104,7 +113,8 @@ internal class DefaultSetReadMarkersTask @Inject constructor(
) { ) {
if (markers[READ_MARKER] == null) { if (markers[READ_MARKER] == null) {
if (readReceiptEventId != null) { if (readReceiptEventId != null) {
roomAPI.sendReceipt(params.roomId, READ_RECEIPT, readReceiptEventId) val readBody = ReadBody(threadId = params.readReceiptThreadId)
roomAPI.sendReceipt(params.roomId, READ_RECEIPT, readReceiptEventId, readBody)
} }
} else { } else {
// "m.fully_read" value is mandatory to make this call // "m.fully_read" value is mandatory to make this call
@ -119,7 +129,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(
TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId
} }
private suspend fun updateDatabase(roomId: String, markers: Map<String, String>, shouldUpdateRoomSummary: Boolean) { private suspend fun updateDatabase(roomId: String, readReceiptThreadId: String?, markers: Map<String, String>, shouldUpdateRoomSummary: Boolean) {
monarchy.awaitTransaction { realm -> monarchy.awaitTransaction { realm ->
val readMarkerId = markers[READ_MARKER] val readMarkerId = markers[READ_MARKER]
val readReceiptId = markers[READ_RECEIPT] val readReceiptId = markers[READ_RECEIPT]
@ -127,7 +137,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(
roomFullyReadHandler.handle(realm, roomId, FullyReadContent(readMarkerId)) roomFullyReadHandler.handle(realm, roomId, FullyReadContent(readMarkerId))
} }
if (readReceiptId != null) { if (readReceiptId != null) {
val readReceiptContent = ReadReceiptHandler.createContent(userId, readReceiptId, clock.epochMillis()) val readReceiptContent = ReadReceiptHandler.createContent(userId, readReceiptId, readReceiptThreadId, clock.epochMillis())
readReceiptHandler.handle(realm, roomId, readReceiptContent, false, null) readReceiptHandler.handle(realm, roomId, readReceiptContent, false, null)
} }
if (shouldUpdateRoomSummary) { if (shouldUpdateRoomSummary) {

View File

@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.content.EncryptionEventContent import org.matrix.android.sdk.api.session.events.model.content.EncryptionEventContent
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataTypes import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataTypes
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
@ -75,7 +76,8 @@ internal class RoomSummaryUpdater @Inject constructor(
private val roomAvatarResolver: RoomAvatarResolver, private val roomAvatarResolver: RoomAvatarResolver,
private val eventDecryptor: EventDecryptor, private val eventDecryptor: EventDecryptor,
private val crossSigningService: DefaultCrossSigningService, private val crossSigningService: DefaultCrossSigningService,
private val roomAccountDataDataSource: RoomAccountDataDataSource private val roomAccountDataDataSource: RoomAccountDataDataSource,
private val homeServerCapabilitiesService: HomeServerCapabilitiesService,
) { ) {
fun refreshLatestPreviewContent(realm: Realm, roomId: String) { fun refreshLatestPreviewContent(realm: Realm, roomId: String) {
@ -151,9 +153,11 @@ internal class RoomSummaryUpdater @Inject constructor(
latestPreviewableEvent.attemptToDecrypt() latestPreviewableEvent.attemptToDecrypt()
} }
val shouldCheckIfReadInEventsThread = homeServerCapabilitiesService.getHomeServerCapabilities().canUseThreadReadReceiptsAndNotifications
roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 || roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 ||
// avoid this call if we are sure there are unread events // avoid this call if we are sure there are unread events
latestPreviewableEvent?.let { !isEventRead(realm.configuration, userId, roomId, it.eventId) } ?: false latestPreviewableEvent?.let { !isEventRead(realm.configuration, userId, roomId, it.eventId, shouldCheckIfReadInEventsThread) } ?: false
roomSummaryEntity.setDisplayName(roomDisplayNameResolver.resolve(realm, roomId)) roomSummaryEntity.setDisplayName(roomDisplayNameResolver.resolve(realm, roomId))
roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(realm, roomId) roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(realm, roomId)

View File

@ -411,7 +411,7 @@ internal class DefaultTimeline(
private fun ensureReadReceiptAreLoaded(realm: Realm) { private fun ensureReadReceiptAreLoaded(realm: Realm) {
readReceiptHandler.getContentFromInitSync(roomId) readReceiptHandler.getContentFromInitSync(roomId)
?.also { ?.also {
Timber.w("INIT_SYNC Insert when opening timeline RR for room $roomId") Timber.d("INIT_SYNC Insert when opening timeline RR for room $roomId")
} }
?.let { readReceiptContent -> ?.let { readReceiptContent ->
realm.executeTransactionAsync { realm.executeTransactionAsync {

View File

@ -33,10 +33,11 @@ import javax.inject.Inject
// value : dict key $UserId // value : dict key $UserId
// value dict key ts // value dict key ts
// dict value ts value // dict value ts value
internal typealias ReadReceiptContent = Map<String, Map<String, Map<String, Map<String, Double>>>> internal typealias ReadReceiptContent = Map<String, Map<String, Map<String, Map<String, Any>>>>
private const val READ_KEY = "m.read" private const val READ_KEY = "m.read"
private const val TIMESTAMP_KEY = "ts" private const val TIMESTAMP_KEY = "ts"
private const val THREAD_ID_KEY = "thread_id"
internal class ReadReceiptHandler @Inject constructor( internal class ReadReceiptHandler @Inject constructor(
private val roomSyncEphemeralTemporaryStore: RoomSyncEphemeralTemporaryStore private val roomSyncEphemeralTemporaryStore: RoomSyncEphemeralTemporaryStore
@ -47,14 +48,19 @@ internal class ReadReceiptHandler @Inject constructor(
fun createContent( fun createContent(
userId: String, userId: String,
eventId: String, eventId: String,
threadId: String?,
currentTimeMillis: Long currentTimeMillis: Long
): ReadReceiptContent { ): ReadReceiptContent {
val userReadReceipt = mutableMapOf<String, Any>(
TIMESTAMP_KEY to currentTimeMillis.toDouble(),
)
threadId?.let {
userReadReceipt.put(THREAD_ID_KEY, threadId)
}
return mapOf( return mapOf(
eventId to mapOf( eventId to mapOf(
READ_KEY to mapOf( READ_KEY to mapOf(
userId to mapOf( userId to userReadReceipt
TIMESTAMP_KEY to currentTimeMillis.toDouble()
)
) )
) )
) )
@ -98,8 +104,9 @@ internal class ReadReceiptHandler @Inject constructor(
val readReceiptsSummary = ReadReceiptsSummaryEntity(eventId = eventId, roomId = roomId) val readReceiptsSummary = ReadReceiptsSummaryEntity(eventId = eventId, roomId = roomId)
for ((userId, paramsDict) in userIdsDict) { for ((userId, paramsDict) in userIdsDict) {
val ts = paramsDict[TIMESTAMP_KEY] ?: 0.0 val ts = paramsDict[TIMESTAMP_KEY] as? Double ?: 0.0
val receiptEntity = ReadReceiptEntity.createUnmanaged(roomId, eventId, userId, ts) val threadId = paramsDict[THREAD_ID_KEY] as String?
val receiptEntity = ReadReceiptEntity.createUnmanaged(roomId, eventId, userId, threadId, ts)
readReceiptsSummary.readReceipts.add(receiptEntity) readReceiptsSummary.readReceipts.add(receiptEntity)
} }
readReceiptSummaries.add(readReceiptsSummary) readReceiptSummaries.add(readReceiptsSummary)
@ -115,7 +122,7 @@ internal class ReadReceiptHandler @Inject constructor(
) { ) {
// First check if we have data from init sync to handle // First check if we have data from init sync to handle
getContentFromInitSync(roomId)?.let { getContentFromInitSync(roomId)?.let {
Timber.w("INIT_SYNC Insert during incremental sync RR for room $roomId") Timber.d("INIT_SYNC Insert during incremental sync RR for room $roomId")
doIncrementalSyncStrategy(realm, roomId, it) doIncrementalSyncStrategy(realm, roomId, it)
aggregator?.ephemeralFilesToDelete?.add(roomId) aggregator?.ephemeralFilesToDelete?.add(roomId)
} }
@ -132,8 +139,9 @@ internal class ReadReceiptHandler @Inject constructor(
} }
for ((userId, paramsDict) in userIdsDict) { for ((userId, paramsDict) in userIdsDict) {
val ts = paramsDict[TIMESTAMP_KEY] ?: 0.0 val ts = paramsDict[TIMESTAMP_KEY] as? Double ?: 0.0
val receiptEntity = ReadReceiptEntity.getOrCreate(realm, roomId, userId) val threadId = paramsDict[THREAD_ID_KEY] as String?
val receiptEntity = ReadReceiptEntity.getOrCreate(realm, roomId, userId, threadId)
// ensure new ts is superior to the previous one // ensure new ts is superior to the previous one
if (ts > receiptEntity.originServerTs) { if (ts > receiptEntity.originServerTs) {
ReadReceiptsSummaryEntity.where(realm, receiptEntity.eventId).findFirst()?.also { ReadReceiptsSummaryEntity.where(realm, receiptEntity.eventId).findFirst()?.also {

View File

@ -217,7 +217,7 @@ class TimelineViewModel @AssistedInject constructor(
observePowerLevel() observePowerLevel()
setupPreviewUrlObservers() setupPreviewUrlObservers()
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
tryOrNull { room.readService().markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT) } tryOrNull { room.readService().markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT, mainTimeLineOnly = true) }
} }
// Inform the SDK that the room is displayed // Inform the SDK that the room is displayed
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
@ -1103,7 +1103,8 @@ class TimelineViewModel @AssistedInject constructor(
} }
bufferedMostRecentDisplayedEvent.root.eventId?.let { eventId -> bufferedMostRecentDisplayedEvent.root.eventId?.let { eventId ->
session.coroutineScope.launch { session.coroutineScope.launch {
tryOrNull { room.readService().setReadReceipt(eventId) } val threadId = initialState.rootThreadEventId ?: ReadService.THREAD_ID_MAIN
tryOrNull { room.readService().setReadReceipt(eventId, threadId = threadId) }
} }
} }
} }
@ -1121,7 +1122,7 @@ class TimelineViewModel @AssistedInject constructor(
if (room == null) return if (room == null) return
setState { copy(unreadState = UnreadState.HasNoUnread) } setState { copy(unreadState = UnreadState.HasNoUnread) }
viewModelScope.launch { viewModelScope.launch {
tryOrNull { room.readService().markAsRead(ReadService.MarkAsReadParams.BOTH) } tryOrNull { room.readService().markAsRead(ReadService.MarkAsReadParams.BOTH, mainTimeLineOnly = true) }
} }
} }

View File

@ -74,6 +74,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
import org.matrix.android.sdk.api.session.room.read.ReadService
import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import timber.log.Timber import timber.log.Timber
@ -516,7 +517,7 @@ class TimelineEventController @Inject constructor(
event.eventId, event.eventId,
readReceipts, readReceipts,
callback, callback,
partialState.isFromThreadTimeline() partialState.isFromThreadTimeline(),
), ),
formattedDayModel = formattedDayModel, formattedDayModel = formattedDayModel,
mergedHeaderModel = mergedHeaderModel mergedHeaderModel = mergedHeaderModel
@ -559,7 +560,7 @@ class TimelineEventController @Inject constructor(
val event = itr.previous() val event = itr.previous()
timelineEventsGroups.addOrIgnore(event) timelineEventsGroups.addOrIgnore(event)
val currentReadReceipts = ArrayList(event.readReceipts).filter { val currentReadReceipts = ArrayList(event.readReceipts).filter {
it.roomMember.userId != session.myUserId it.roomMember.userId != session.myUserId && it.isVisibleInThisThread()
} }
if (timelineEventVisibilityHelper.shouldShowEvent( if (timelineEventVisibilityHelper.shouldShowEvent(
timelineEvent = event, timelineEvent = event,
@ -577,6 +578,14 @@ class TimelineEventController @Inject constructor(
} }
} }
private fun ReadReceipt.isVisibleInThisThread(): Boolean {
return if (partialState.isFromThreadTimeline()) {
this.threadId == partialState.rootThreadEventId
} else {
this.threadId == null || this.threadId == ReadService.THREAD_ID_MAIN
}
}
private fun buildDaySeparatorItem(originServerTs: Long?): DaySeparatorItem { private fun buildDaySeparatorItem(originServerTs: Long?): DaySeparatorItem {
val formattedDay = dateFormatter.format(originServerTs, DateFormatKind.TIMELINE_DAY_DIVIDER) val formattedDay = dateFormatter.format(originServerTs, DateFormatKind.TIMELINE_DAY_DIVIDER)
return DaySeparatorItem_().formattedDay(formattedDay).id(formattedDay) return DaySeparatorItem_().formattedDay(formattedDay).id(formattedDay)

View File

@ -21,16 +21,20 @@ import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem_ import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem_
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.model.ReadReceipt import org.matrix.android.sdk.api.session.room.model.ReadReceipt
import javax.inject.Inject import javax.inject.Inject
class ReadReceiptsItemFactory @Inject constructor(private val avatarRenderer: AvatarRenderer) { class ReadReceiptsItemFactory @Inject constructor(
private val avatarRenderer: AvatarRenderer,
private val session: Session
) {
fun create( fun create(
eventId: String, eventId: String,
readReceipts: List<ReadReceipt>, readReceipts: List<ReadReceipt>,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
isFromThreadTimeLine: Boolean isFromThreadTimeLine: Boolean,
): ReadReceiptsItem? { ): ReadReceiptsItem? {
if (readReceipts.isEmpty()) { if (readReceipts.isEmpty()) {
return null return null
@ -40,12 +44,13 @@ class ReadReceiptsItemFactory @Inject constructor(private val avatarRenderer: Av
ReadReceiptData(it.roomMember.userId, it.roomMember.avatarUrl, it.roomMember.displayName, it.originServerTs) ReadReceiptData(it.roomMember.userId, it.roomMember.avatarUrl, it.roomMember.displayName, it.originServerTs)
} }
.sortedByDescending { it.timestamp } .sortedByDescending { it.timestamp }
val threadReadReceiptsSupported = session.homeServerCapabilitiesService().getHomeServerCapabilities().canUseThreadReadReceiptsAndNotifications
return ReadReceiptsItem_() return ReadReceiptsItem_()
.id("read_receipts_$eventId") .id("read_receipts_$eventId")
.eventId(eventId) .eventId(eventId)
.readReceipts(readReceiptsData) .readReceipts(readReceiptsData)
.avatarRenderer(avatarRenderer) .avatarRenderer(avatarRenderer)
.shouldHideReadReceipts(isFromThreadTimeLine) .shouldHideReadReceipts(isFromThreadTimeLine && !threadReadReceiptsSupported)
.clickListener { .clickListener {
callback?.onReadReceiptsClicked(readReceiptsData) callback?.onReadReceiptsClicked(readReceiptsData)
} }

View File

@ -109,7 +109,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
val room = session.getRoom(roomId) val room = session.getRoom(roomId)
if (room != null) { if (room != null) {
session.coroutineScope.launch { session.coroutineScope.launch {
tryOrNull { room.readService().markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT) } tryOrNull { room.readService().markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT, mainTimeLineOnly = false) }
} }
} }
} }