diff --git a/changelog.d/6996.sdk b/changelog.d/6996.sdk deleted file mode 100644 index 588ec160d78..00000000000 --- a/changelog.d/6996.sdk +++ /dev/null @@ -1 +0,0 @@ -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) diff --git a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt index 7ad342b22fd..a6b4cc98a67 100644 --- a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt +++ b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt @@ -100,8 +100,8 @@ class FlowRoom(private val room: Room) { return room.readService().getReadMarkerLive().asFlow() } - fun liveReadReceipt(threadId: String?): Flow> { - return room.readService().getMyReadReceiptLive(threadId).asFlow() + fun liveReadReceipt(): Flow> { + return room.readService().getMyReadReceiptLive().asFlow() } fun liveEventReadReceipts(eventId: String): Flow> { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReadReceipt.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReadReceipt.kt index da7e4ea9286..5639730219e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReadReceipt.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReadReceipt.kt @@ -18,6 +18,5 @@ package org.matrix.android.sdk.api.session.room.model data class ReadReceipt( val roomMember: RoomMemberSummary, - val originServerTs: Long, - val threadId: String? + val originServerTs: Long ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt index 83680ec2d89..dac1a1a773e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt @@ -34,14 +34,12 @@ interface ReadService { /** * Force the read marker to be set on the latest event. */ - suspend fun markAsRead(params: MarkAsReadParams = MarkAsReadParams.BOTH, mainTimeLineOnly: Boolean = true) + suspend fun markAsRead(params: MarkAsReadParams = MarkAsReadParams.BOTH) /** * 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, threadId: String) + suspend fun setReadReceipt(eventId: String) /** * Set the read marker on the event with provided eventId. @@ -61,10 +59,10 @@ interface ReadService { /** * Returns a live read receipt id for the room. */ - fun getMyReadReceiptLive(threadId: String?): LiveData> + fun getMyReadReceiptLive(): LiveData> /** - * Get the eventId from the main timeline where the read receipt for the provided user is. + * Get the eventId where the read receipt for the provided user is. * @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 @@ -76,8 +74,4 @@ interface ReadService { * @param eventId the event */ fun getEventReadReceiptsLive(eventId: String): LiveData> - - companion object { - const val THREAD_ID_MAIN = "main" - } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt index 43f84e771ab..149a2eebfea 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt @@ -132,7 +132,7 @@ private fun handleReadReceipts(realm: Realm, roomId: String, eventEntity: EventE val originServerTs = eventEntity.originServerTs if (originServerTs != null) { val timestampOfEvent = originServerTs.toDouble() - val readReceiptOfSender = ReadReceiptEntity.getOrCreate(realm, roomId = roomId, userId = senderId, threadId = eventEntity.rootThreadEventId) + val readReceiptOfSender = ReadReceiptEntity.getOrCreate(realm, roomId = roomId, userId = senderId) // If the synced RR is older, update if (timestampOfEvent > readReceiptOfSender.originServerTs) { val previousReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId = readReceiptOfSender.eventId).findFirst() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt index 7999a2ea140..dfac7f67087 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt @@ -65,12 +65,12 @@ internal fun Map.updateThreadSummaryIfNeeded( inThreadMessages = inThreadMessages, latestMessageTimelineEventEntity = latestEventInThread ) - - if (shouldUpdateNotifications) { - updateThreadNotifications(roomId, realm, currentUserId, rootThreadEventId) - } } } + + if (shouldUpdateNotifications) { + updateNotificationsNew(roomId, realm, currentUserId) + } } /** @@ -273,8 +273,8 @@ internal fun TimelineEventEntity.Companion.isUserMentionedInThread(realm: Realm, /** * Find the read receipt for the current user. */ -internal fun findMyReadReceipt(realm: Realm, roomId: String, userId: String, threadId: String?): String? = - ReadReceiptEntity.where(realm, roomId = roomId, userId = userId, threadId = threadId) +internal fun findMyReadReceipt(realm: Realm, roomId: String, userId: String): String? = + ReadReceiptEntity.where(realm, roomId = roomId, userId = userId) .findFirst() ?.eventId @@ -293,29 +293,28 @@ internal fun isUserMentioned(currentUserId: String, timelineEventEntity: Timelin * Important: It will work only with the latest chunk, while read marker will be changed * immediately so we should not display wrong notifications */ -internal fun updateThreadNotifications(roomId: String, realm: Realm, currentUserId: String, rootThreadEventId: String) { - val readReceipt = findMyReadReceipt(realm, roomId, currentUserId, threadId = rootThreadEventId) ?: return +internal fun updateNotificationsNew(roomId: String, realm: Realm, currentUserId: String) { + val readReceipt = findMyReadReceipt(realm, roomId, currentUserId) ?: return val readReceiptChunk = ChunkEntity .findIncludingEvent(realm, readReceipt) ?: return - val readReceiptChunkThreadEvents = readReceiptChunk + val readReceiptChunkTimelineEvents = readReceiptChunk .timelineEvents .where() .equalTo(TimelineEventEntityFields.ROOM_ID, roomId) - .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId) .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING) .findAll() ?: return - val readReceiptChunkPosition = readReceiptChunkThreadEvents.indexOfFirst { it.eventId == readReceipt } + val readReceiptChunkPosition = readReceiptChunkTimelineEvents.indexOfFirst { it.eventId == readReceipt } if (readReceiptChunkPosition == -1) return - if (readReceiptChunkPosition < readReceiptChunkThreadEvents.lastIndex) { + if (readReceiptChunkPosition < readReceiptChunkTimelineEvents.lastIndex) { // If the read receipt is found inside the chunk - val threadEventsAfterReadReceipt = readReceiptChunkThreadEvents - .slice(readReceiptChunkPosition..readReceiptChunkThreadEvents.lastIndex) + val threadEventsAfterReadReceipt = readReceiptChunkTimelineEvents + .slice(readReceiptChunkPosition..readReceiptChunkTimelineEvents.lastIndex) .filter { it.root?.isThread() == true } // In order for the below code to work for old events, we should save the previous read receipt @@ -344,21 +343,26 @@ internal fun updateThreadNotifications(roomId: String, realm: Realm, currentUser it.root?.rootThreadEventId } - // Update root thread event only if the user have participated in - val isUserParticipating = TimelineEventEntity.isUserParticipatingInThread( - realm = realm, - roomId = roomId, - rootThreadEventId = rootThreadEventId, - senderId = currentUserId - ) - val rootThreadEventEntity = EventEntity.where(realm, rootThreadEventId).findFirst() - - if (isUserParticipating) { - rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_MESSAGE - } + // Find the root events in the new thread events + val rootThreads = threadEventsAfterReadReceipt.distinctBy { it.root?.rootThreadEventId }.mapNotNull { it.root?.rootThreadEventId } - if (userMentionsList.contains(rootThreadEventId)) { - rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE + // Update root thread events only if the user have participated in + rootThreads.forEach { eventId -> + val isUserParticipating = TimelineEventEntity.isUserParticipatingInThread( + realm = realm, + roomId = roomId, + rootThreadEventId = eventId, + senderId = currentUserId + ) + val rootThreadEventEntity = EventEntity.where(realm, eventId).findFirst() + + if (isUserParticipating) { + rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_MESSAGE + } + + if (userMentionsList.contains(eventId)) { + rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE + } } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt index 3b71ae3dea2..2be4510b6fb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt @@ -50,7 +50,7 @@ internal class ReadReceiptsSummaryMapper @Inject constructor( .mapNotNull { val roomMember = RoomMemberSummaryEntity.where(realm, roomId = it.roomId, userId = it.userId).findFirst() ?: return@mapNotNull null - ReadReceipt(roomMember.asDomain(), it.originServerTs.toLong(), it.threadId) + ReadReceipt(roomMember.asDomain(), it.originServerTs.toLong()) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadReceiptEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadReceiptEntity.kt index cedd5e74242..9623c953597 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadReceiptEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadReceiptEntity.kt @@ -26,7 +26,6 @@ internal open class ReadReceiptEntity( var eventId: String = "", var roomId: String = "", var userId: String = "", - var threadId: String? = null, var originServerTs: Double = 0.0 ) : RealmObject() { companion object diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt index 1deca47b70a..c8f22dc2ccb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt @@ -20,7 +20,6 @@ import io.realm.RealmObject import io.realm.RealmResults import io.realm.annotations.Index import io.realm.annotations.LinkingObjects -import org.matrix.android.sdk.api.session.room.read.ReadService import org.matrix.android.sdk.internal.extensions.assertIsManaged internal open class TimelineEventEntity( @@ -53,7 +52,3 @@ internal fun TimelineEventEntity.deleteOnCascade(canDeleteRoot: Boolean) { } deleteFromRealm() } - -internal fun TimelineEventEntity.getThreadId(): String { - return root?.rootThreadEventId ?: ReadService.THREAD_ID_MAIN -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt index ebfe23105e3..0b0f01a67de 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt @@ -18,20 +18,17 @@ package org.matrix.android.sdk.internal.database.query import io.realm.Realm import io.realm.RealmConfiguration 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.model.ChunkEntity 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.TimelineEventEntity -import org.matrix.android.sdk.internal.database.model.getThreadId internal fun isEventRead( realmConfiguration: RealmConfiguration, userId: String?, roomId: String?, - eventId: String?, - shouldCheckIfReadInEventsThread: Boolean + eventId: String? ): Boolean { if (userId.isNullOrBlank() || roomId.isNullOrBlank() || eventId.isNullOrBlank()) { return false @@ -48,8 +45,7 @@ internal fun isEventRead( eventToCheck.root?.sender == userId -> true // If new event exists and the latest event is from ourselves we can infer the event is read latestEventIsFromSelf(realm, roomId, userId) -> true - eventToCheck.isBeforeLatestReadReceipt(realm, roomId, userId, null) -> true - (shouldCheckIfReadInEventsThread && eventToCheck.isBeforeLatestReadReceipt(realm, roomId, userId, eventToCheck.getThreadId())) -> true + eventToCheck.isBeforeLatestReadReceipt(realm, roomId, userId) -> true else -> false } } @@ -58,33 +54,27 @@ internal fun isEventRead( private fun latestEventIsFromSelf(realm: Realm, roomId: String, userId: String) = TimelineEventEntity.latestEvent(realm, roomId, true) ?.root?.sender == userId -private fun TimelineEventEntity.isBeforeLatestReadReceipt(realm: Realm, roomId: String, userId: String, threadId: String?): Boolean { - val isMoreRecent = ReadReceiptEntity.where(realm, roomId, userId, threadId).findFirst()?.let { readReceipt -> +private fun TimelineEventEntity.isBeforeLatestReadReceipt(realm: Realm, roomId: String, userId: String): Boolean { + return ReadReceiptEntity.where(realm, roomId, userId).findFirst()?.let { readReceipt -> val readReceiptEvent = TimelineEventEntity.where(realm, roomId, readReceipt.eventId).findFirst() readReceiptEvent?.isMoreRecentThan(this) } ?: false - return isMoreRecent } /** * 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. */ -private fun hasReadMissingEvent(realm: Realm, - 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 hasReadMissingEvent(realm: Realm, latestChunkEntity: ChunkEntity, roomId: String, userId: String, eventId: String): Boolean { + return realm.doesEventExistInChunkHistory(eventId) && realm.hasReadReceiptInLatestChunk(latestChunkEntity, roomId, userId) } private fun Realm.doesEventExistInChunkHistory(eventId: String): Boolean { return ChunkEntity.findIncludingEvent(this, eventId) != null } -private fun Realm.hasReadReceiptInLatestChunk(latestChunkEntity: ChunkEntity, roomId: String, userId: String, threadId: String?): Boolean { - return ReadReceiptEntity.where(this, roomId = roomId, userId = userId, threadId = threadId).findFirst()?.let { +private fun Realm.hasReadReceiptInLatestChunk(latestChunkEntity: ChunkEntity, roomId: String, userId: String): Boolean { + return ReadReceiptEntity.where(this, roomId = roomId, userId = userId).findFirst()?.let { latestChunkEntity.timelineEvents.find(it.eventId) } != null } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt index 0f9f56b938d..170814d3f28 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt @@ -20,20 +20,12 @@ import io.realm.Realm import io.realm.RealmQuery import io.realm.kotlin.createObject 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.ReadReceiptEntityFields -internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, userId: String, threadId: String?): RealmQuery { +internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, userId: String): RealmQuery { return realm.where() - .equalTo(ReadReceiptEntityFields.PRIMARY_KEY, buildPrimaryKey(roomId, userId, threadId)) -} - -internal fun ReadReceiptEntity.Companion.forMainTimelineWhere(realm: Realm, roomId: String, userId: String): RealmQuery { - return realm.where() - .equalTo(ReadReceiptEntityFields.PRIMARY_KEY, buildPrimaryKey(roomId, userId, ReadService.THREAD_ID_MAIN)) - .or() - .equalTo(ReadReceiptEntityFields.PRIMARY_KEY, buildPrimaryKey(roomId, userId, null)) + .equalTo(ReadReceiptEntityFields.PRIMARY_KEY, buildPrimaryKey(roomId, userId)) } internal fun ReadReceiptEntity.Companion.whereUserId(realm: Realm, userId: String): RealmQuery { @@ -46,37 +38,23 @@ internal fun ReadReceiptEntity.Companion.whereRoomId(realm: Realm, roomId: Strin .equalTo(ReadReceiptEntityFields.ROOM_ID, roomId) } -internal fun ReadReceiptEntity.Companion.createUnmanaged( - roomId: String, - eventId: String, - userId: String, - threadId: String?, - originServerTs: Double -): ReadReceiptEntity { +internal fun ReadReceiptEntity.Companion.createUnmanaged(roomId: String, eventId: String, userId: String, originServerTs: Double): ReadReceiptEntity { return ReadReceiptEntity().apply { - this.primaryKey = buildPrimaryKey(roomId, userId, threadId) + this.primaryKey = "${roomId}_$userId" this.eventId = eventId this.roomId = roomId this.userId = userId - this.threadId = threadId this.originServerTs = originServerTs } } -internal fun ReadReceiptEntity.Companion.getOrCreate(realm: Realm, roomId: String, userId: String, threadId: String?): ReadReceiptEntity { - return ReadReceiptEntity.where(realm, roomId, userId, threadId).findFirst() - ?: realm.createObject(buildPrimaryKey(roomId, userId, threadId)) +internal fun ReadReceiptEntity.Companion.getOrCreate(realm: Realm, roomId: String, userId: String): ReadReceiptEntity { + return ReadReceiptEntity.where(realm, roomId, userId).findFirst() + ?: realm.createObject(buildPrimaryKey(roomId, userId)) .apply { this.roomId = roomId this.userId = userId - this.threadId = threadId } } -private fun buildPrimaryKey(roomId: String, userId: String, threadId: String?): String { - return if (threadId == null) { - "${roomId}_${userId}" - } else { - "${roomId}_${userId}_${threadId}" - } -} +private fun buildPrimaryKey(roomId: String, userId: String) = "${roomId}_$userId" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt index 31bed90b622..9bcb7b8e4c7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt @@ -33,7 +33,6 @@ 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.joining.InviteBody 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.reporting.ReportContentBody import org.matrix.android.sdk.internal.session.room.send.SendResponse @@ -174,7 +173,7 @@ internal interface RoomAPI { @Path("roomId") roomId: String, @Path("receiptType") receiptType: String, @Path("eventId") eventId: String, - @Body body: ReadBody + @Body body: JsonDict = emptyMap() ) /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt index 36ec5e8dacd..b30c66c82ed 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt @@ -30,20 +30,17 @@ 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.ReadReceiptEntity 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.where import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource internal class DefaultReadService @AssistedInject constructor( @Assisted private val roomId: String, @SessionDatabase private val monarchy: Monarchy, private val setReadMarkersTask: SetReadMarkersTask, private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, - @UserId private val userId: String, - private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource + @UserId private val userId: String ) : ReadService { @AssistedFactory @@ -51,28 +48,17 @@ internal class DefaultReadService @AssistedInject constructor( fun create(roomId: String): DefaultReadService } - 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 - } + override suspend fun markAsRead(params: ReadService.MarkAsReadParams) { val taskParams = SetReadMarkersTask.Params( roomId = roomId, forceReadMarker = params.forceReadMarker(), - forceReadReceipt = params.forceReadReceipt(), - readReceiptThreadId = readReceiptThreadId + forceReadReceipt = params.forceReadReceipt() ) setReadMarkersTask.execute(taskParams) } - override suspend fun setReadReceipt(eventId: String, threadId: String) { - val readReceiptThreadId = if (homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.canUseThreadReadReceiptsAndNotifications == true) { - threadId - } else { - null - } - val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = null, readReceiptEventId = eventId, readReceiptThreadId = readReceiptThreadId) + override suspend fun setReadReceipt(eventId: String) { + val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = null, readReceiptEventId = eventId) setReadMarkersTask.execute(params) } @@ -82,8 +68,7 @@ internal class DefaultReadService @AssistedInject constructor( } override fun isEventRead(eventId: String): Boolean { - val shouldCheckIfReadInEventsThread = homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.canUseThreadReadReceiptsAndNotifications == true - return isEventRead(monarchy.realmConfiguration, userId, roomId, eventId, shouldCheckIfReadInEventsThread) + return isEventRead(monarchy.realmConfiguration, userId, roomId, eventId) } override fun getReadMarkerLive(): LiveData> { @@ -96,9 +81,9 @@ internal class DefaultReadService @AssistedInject constructor( } } - override fun getMyReadReceiptLive(threadId: String?): LiveData> { + override fun getMyReadReceiptLive(): LiveData> { val liveRealmData = monarchy.findAllMappedWithChanges( - { ReadReceiptEntity.where(it, roomId = roomId, userId = userId, threadId = threadId) }, + { ReadReceiptEntity.where(it, roomId = roomId, userId = userId) }, { it.eventId } ) return Transformations.map(liveRealmData) { @@ -109,11 +94,10 @@ internal class DefaultReadService @AssistedInject constructor( override fun getUserReadReceipt(userId: String): String? { var eventId: String? = null monarchy.doWithRealm { - eventId = ReadReceiptEntity.forMainTimelineWhere(it, roomId = roomId, userId = userId) + eventId = ReadReceiptEntity.where(it, roomId = roomId, userId = userId) .findFirst() ?.eventId } - return eventId } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/ReadBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/ReadBody.kt deleted file mode 100644 index 9374de5d5fc..00000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/ReadBody.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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?, -) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt index 8e7592a8b4d..a124a8a4c25 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt @@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.session.room.read import com.zhuinden.monarchy.Monarchy import io.realm.Realm 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.TimelineEventEntity import org.matrix.android.sdk.internal.database.query.isEventRead @@ -46,9 +45,8 @@ internal interface SetReadMarkersTask : Task { val roomId: String, val fullyReadEventId: String? = null, val readReceiptEventId: String? = null, - val readReceiptThreadId: String? = null, val forceReadReceipt: Boolean = false, - val forceReadMarker: Boolean = false, + val forceReadMarker: Boolean = false ) } @@ -63,14 +61,12 @@ internal class DefaultSetReadMarkersTask @Inject constructor( @UserId private val userId: String, private val globalErrorReceiver: GlobalErrorReceiver, private val clock: Clock, - private val homeServerCapabilitiesService: HomeServerCapabilitiesService, ) : SetReadMarkersTask { override suspend fun execute(params: SetReadMarkersTask.Params) { val markers = mutableMapOf() Timber.v("Execute set read marker with params: $params") val latestSyncedEventId = latestSyncedEventId(params.roomId) - val readReceiptThreadId = params.readReceiptThreadId val fullyReadEventId = if (params.forceReadMarker) { latestSyncedEventId } else { @@ -81,7 +77,6 @@ internal class DefaultSetReadMarkersTask @Inject constructor( } else { params.readReceiptEventId } - if (fullyReadEventId != null && !isReadMarkerMoreRecent(monarchy.realmConfiguration, params.roomId, fullyReadEventId)) { if (LocalEcho.isLocalEchoId(fullyReadEventId)) { Timber.w("Can't set read marker for local event $fullyReadEventId") @@ -89,12 +84,8 @@ internal class DefaultSetReadMarkersTask @Inject constructor( markers[READ_MARKER] = fullyReadEventId } } - - val shouldCheckIfReadInEventsThread = readReceiptThreadId != null && - homeServerCapabilitiesService.getHomeServerCapabilities().canUseThreadReadReceiptsAndNotifications - if (readReceiptEventId != null && - !isEventRead(monarchy.realmConfiguration, userId, params.roomId, readReceiptEventId, shouldCheckIfReadInEventsThread)) { + !isEventRead(monarchy.realmConfiguration, userId, params.roomId, readReceiptEventId)) { if (LocalEcho.isLocalEchoId(readReceiptEventId)) { Timber.w("Can't set read receipt for local event $readReceiptEventId") } else { @@ -104,7 +95,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor( val shouldUpdateRoomSummary = readReceiptEventId != null && readReceiptEventId == latestSyncedEventId if (markers.isNotEmpty() || shouldUpdateRoomSummary) { - updateDatabase(params.roomId, readReceiptThreadId, markers, shouldUpdateRoomSummary) + updateDatabase(params.roomId, markers, shouldUpdateRoomSummary) } if (markers.isNotEmpty()) { executeRequest( @@ -113,8 +104,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor( ) { if (markers[READ_MARKER] == null) { if (readReceiptEventId != null) { - val readBody = ReadBody(threadId = params.readReceiptThreadId) - roomAPI.sendReceipt(params.roomId, READ_RECEIPT, readReceiptEventId, readBody) + roomAPI.sendReceipt(params.roomId, READ_RECEIPT, readReceiptEventId) } } else { // "m.fully_read" value is mandatory to make this call @@ -129,7 +119,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor( TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId } - private suspend fun updateDatabase(roomId: String, readReceiptThreadId: String?, markers: Map, shouldUpdateRoomSummary: Boolean) { + private suspend fun updateDatabase(roomId: String, markers: Map, shouldUpdateRoomSummary: Boolean) { monarchy.awaitTransaction { realm -> val readMarkerId = markers[READ_MARKER] val readReceiptId = markers[READ_RECEIPT] @@ -137,7 +127,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor( roomFullyReadHandler.handle(realm, roomId, FullyReadContent(readMarkerId)) } if (readReceiptId != null) { - val readReceiptContent = ReadReceiptHandler.createContent(userId, readReceiptId, readReceiptThreadId, clock.epochMillis()) + val readReceiptContent = ReadReceiptHandler.createContent(userId, readReceiptId, clock.epochMillis()) readReceiptHandler.handle(realm, roomId, readReceiptContent, false, null) } if (shouldUpdateRoomSummary) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt index 21a0862c652..7c83a4afa79 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt @@ -24,7 +24,6 @@ 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.content.EncryptionEventContent 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.model.Membership import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent @@ -76,8 +75,7 @@ internal class RoomSummaryUpdater @Inject constructor( private val roomAvatarResolver: RoomAvatarResolver, private val eventDecryptor: EventDecryptor, private val crossSigningService: DefaultCrossSigningService, - private val roomAccountDataDataSource: RoomAccountDataDataSource, - private val homeServerCapabilitiesService: HomeServerCapabilitiesService, + private val roomAccountDataDataSource: RoomAccountDataDataSource ) { fun refreshLatestPreviewContent(realm: Realm, roomId: String) { @@ -153,11 +151,9 @@ internal class RoomSummaryUpdater @Inject constructor( latestPreviewableEvent.attemptToDecrypt() } - val shouldCheckIfReadInEventsThread = homeServerCapabilitiesService.getHomeServerCapabilities().canUseThreadReadReceiptsAndNotifications - roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 || // avoid this call if we are sure there are unread events - latestPreviewableEvent?.let { !isEventRead(realm.configuration, userId, roomId, it.eventId, shouldCheckIfReadInEventsThread) } ?: false + latestPreviewableEvent?.let { !isEventRead(realm.configuration, userId, roomId, it.eventId) } ?: false roomSummaryEntity.setDisplayName(roomDisplayNameResolver.resolve(realm, roomId)) roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(realm, roomId) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index 0854cc5cf41..c380ccf14f8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -411,7 +411,7 @@ internal class DefaultTimeline( private fun ensureReadReceiptAreLoaded(realm: Realm) { readReceiptHandler.getContentFromInitSync(roomId) ?.also { - Timber.d("INIT_SYNC Insert when opening timeline RR for room $roomId") + Timber.w("INIT_SYNC Insert when opening timeline RR for room $roomId") } ?.let { readReceiptContent -> realm.executeTransactionAsync { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt index 7f12ce653c7..7329611a017 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt @@ -33,11 +33,10 @@ import javax.inject.Inject // value : dict key $UserId // value dict key ts // dict value ts value -internal typealias ReadReceiptContent = Map>>> +internal typealias ReadReceiptContent = Map>>> private const val READ_KEY = "m.read" private const val TIMESTAMP_KEY = "ts" -private const val THREAD_ID_KEY = "thread_id" internal class ReadReceiptHandler @Inject constructor( private val roomSyncEphemeralTemporaryStore: RoomSyncEphemeralTemporaryStore @@ -48,19 +47,14 @@ internal class ReadReceiptHandler @Inject constructor( fun createContent( userId: String, eventId: String, - threadId: String?, currentTimeMillis: Long ): ReadReceiptContent { - val userReadReceipt = mutableMapOf( - TIMESTAMP_KEY to currentTimeMillis.toDouble(), - ) - threadId?.let { - userReadReceipt.put(THREAD_ID_KEY, threadId) - } return mapOf( eventId to mapOf( READ_KEY to mapOf( - userId to userReadReceipt + userId to mapOf( + TIMESTAMP_KEY to currentTimeMillis.toDouble() + ) ) ) ) @@ -104,9 +98,8 @@ internal class ReadReceiptHandler @Inject constructor( val readReceiptsSummary = ReadReceiptsSummaryEntity(eventId = eventId, roomId = roomId) for ((userId, paramsDict) in userIdsDict) { - val ts = paramsDict[TIMESTAMP_KEY] as? Double ?: 0.0 - val threadId = paramsDict[THREAD_ID_KEY] as String? - val receiptEntity = ReadReceiptEntity.createUnmanaged(roomId, eventId, userId, threadId, ts) + val ts = paramsDict[TIMESTAMP_KEY] ?: 0.0 + val receiptEntity = ReadReceiptEntity.createUnmanaged(roomId, eventId, userId, ts) readReceiptsSummary.readReceipts.add(receiptEntity) } readReceiptSummaries.add(readReceiptsSummary) @@ -122,7 +115,7 @@ internal class ReadReceiptHandler @Inject constructor( ) { // First check if we have data from init sync to handle getContentFromInitSync(roomId)?.let { - Timber.d("INIT_SYNC Insert during incremental sync RR for room $roomId") + Timber.w("INIT_SYNC Insert during incremental sync RR for room $roomId") doIncrementalSyncStrategy(realm, roomId, it) aggregator?.ephemeralFilesToDelete?.add(roomId) } @@ -139,9 +132,8 @@ internal class ReadReceiptHandler @Inject constructor( } for ((userId, paramsDict) in userIdsDict) { - val ts = paramsDict[TIMESTAMP_KEY] as? Double ?: 0.0 - val threadId = paramsDict[THREAD_ID_KEY] as String? - val receiptEntity = ReadReceiptEntity.getOrCreate(realm, roomId, userId, threadId) + val ts = paramsDict[TIMESTAMP_KEY] ?: 0.0 + val receiptEntity = ReadReceiptEntity.getOrCreate(realm, roomId, userId) // ensure new ts is superior to the previous one if (ts > receiptEntity.originServerTs) { ReadReceiptsSummaryEntity.where(realm, receiptEntity.eventId).findFirst()?.also { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index 02782783b84..ef238d56e6f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -217,7 +217,7 @@ class TimelineViewModel @AssistedInject constructor( observePowerLevel() setupPreviewUrlObservers() viewModelScope.launch(Dispatchers.IO) { - tryOrNull { room.readService().markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT, mainTimeLineOnly = true) } + tryOrNull { room.readService().markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT) } } // Inform the SDK that the room is displayed viewModelScope.launch(Dispatchers.IO) { @@ -1103,8 +1103,7 @@ class TimelineViewModel @AssistedInject constructor( } bufferedMostRecentDisplayedEvent.root.eventId?.let { eventId -> session.coroutineScope.launch { - val threadId = initialState.rootThreadEventId ?: ReadService.THREAD_ID_MAIN - tryOrNull { room.readService().setReadReceipt(eventId, threadId = threadId) } + tryOrNull { room.readService().setReadReceipt(eventId) } } } } @@ -1122,7 +1121,7 @@ class TimelineViewModel @AssistedInject constructor( if (room == null) return setState { copy(unreadState = UnreadState.HasNoUnread) } viewModelScope.launch { - tryOrNull { room.readService().markAsRead(ReadService.MarkAsReadParams.BOTH, mainTimeLineOnly = true) } + tryOrNull { room.readService().markAsRead(ReadService.MarkAsReadParams.BOTH) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index f845a42dcd7..1f079e420bb 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -74,7 +74,6 @@ 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.MessageImageInfoContent 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.TimelineEvent import timber.log.Timber @@ -517,7 +516,7 @@ class TimelineEventController @Inject constructor( event.eventId, readReceipts, callback, - partialState.isFromThreadTimeline(), + partialState.isFromThreadTimeline() ), formattedDayModel = formattedDayModel, mergedHeaderModel = mergedHeaderModel @@ -560,7 +559,7 @@ class TimelineEventController @Inject constructor( val event = itr.previous() timelineEventsGroups.addOrIgnore(event) val currentReadReceipts = ArrayList(event.readReceipts).filter { - it.roomMember.userId != session.myUserId && it.isVisibleInThisThread() + it.roomMember.userId != session.myUserId } if (timelineEventVisibilityHelper.shouldShowEvent( timelineEvent = event, @@ -578,14 +577,6 @@ 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 { val formattedDay = dateFormatter.format(originServerTs, DateFormatKind.TIMELINE_DAY_DIVIDER) return DaySeparatorItem_().formattedDay(formattedDay).id(formattedDay) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/ReadReceiptsItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/ReadReceiptsItemFactory.kt index 8607af68910..6a711ec2dc7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/ReadReceiptsItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/ReadReceiptsItemFactory.kt @@ -21,20 +21,16 @@ 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.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 javax.inject.Inject -class ReadReceiptsItemFactory @Inject constructor( - private val avatarRenderer: AvatarRenderer, - private val session: Session -) { +class ReadReceiptsItemFactory @Inject constructor(private val avatarRenderer: AvatarRenderer) { fun create( eventId: String, readReceipts: List, callback: TimelineEventController.Callback?, - isFromThreadTimeLine: Boolean, + isFromThreadTimeLine: Boolean ): ReadReceiptsItem? { if (readReceipts.isEmpty()) { return null @@ -44,13 +40,12 @@ class ReadReceiptsItemFactory @Inject constructor( ReadReceiptData(it.roomMember.userId, it.roomMember.avatarUrl, it.roomMember.displayName, it.originServerTs) } .sortedByDescending { it.timestamp } - val threadReadReceiptsSupported = session.homeServerCapabilitiesService().getHomeServerCapabilities().canUseThreadReadReceiptsAndNotifications return ReadReceiptsItem_() .id("read_receipts_$eventId") .eventId(eventId) .readReceipts(readReceiptsData) .avatarRenderer(avatarRenderer) - .shouldHideReadReceipts(isFromThreadTimeLine && !threadReadReceiptsSupported) + .shouldHideReadReceipts(isFromThreadTimeLine) .clickListener { callback?.onReadReceiptsClicked(readReceiptsData) } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt index 180351f8069..3fe0898eb4b 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt @@ -109,7 +109,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { val room = session.getRoom(roomId) if (room != null) { session.coroutineScope.launch { - tryOrNull { room.readService().markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT, mainTimeLineOnly = false) } + tryOrNull { room.readService().markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT) } } } }