Skip to content

Commit 18bcc83

Browse files
authored
added read receipts for threads (#7474)
1 parent 27419f0 commit 18bcc83

File tree

22 files changed

+215
-94
lines changed

22 files changed

+215
-94
lines changed

changelog.d/6996.sdk

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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)

matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,8 @@ class FlowRoom(private val room: Room) {
100100
return room.readService().getReadMarkerLive().asFlow()
101101
}
102102

103-
fun liveReadReceipt(): Flow<Optional<String>> {
104-
return room.readService().getMyReadReceiptLive().asFlow()
103+
fun liveReadReceipt(threadId: String?): Flow<Optional<String>> {
104+
return room.readService().getMyReadReceiptLive(threadId).asFlow()
105105
}
106106

107107
fun liveEventReadReceipts(eventId: String): Flow<List<ReadReceipt>> {

matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReadReceipt.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@ package org.matrix.android.sdk.api.session.room.model
1818

1919
data class ReadReceipt(
2020
val roomMember: RoomMemberSummary,
21-
val originServerTs: Long
21+
val originServerTs: Long,
22+
val threadId: String?
2223
)

matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,14 @@ interface ReadService {
3434
/**
3535
* Force the read marker to be set on the latest event.
3636
*/
37-
suspend fun markAsRead(params: MarkAsReadParams = MarkAsReadParams.BOTH)
37+
suspend fun markAsRead(params: MarkAsReadParams = MarkAsReadParams.BOTH, mainTimeLineOnly: Boolean = true)
3838

3939
/**
4040
* Set the read receipt on the event with provided eventId.
41+
* @param eventId the id of the event where read receipt will be set
42+
* @param threadId the id of the thread in which read receipt will be set. For main thread use [ReadService.THREAD_ID_MAIN] constant
4143
*/
42-
suspend fun setReadReceipt(eventId: String)
44+
suspend fun setReadReceipt(eventId: String, threadId: String)
4345

4446
/**
4547
* Set the read marker on the event with provided eventId.
@@ -59,10 +61,10 @@ interface ReadService {
5961
/**
6062
* Returns a live read receipt id for the room.
6163
*/
62-
fun getMyReadReceiptLive(): LiveData<Optional<String>>
64+
fun getMyReadReceiptLive(threadId: String?): LiveData<Optional<String>>
6365

6466
/**
65-
* Get the eventId where the read receipt for the provided user is.
67+
* Get the eventId from the main timeline where the read receipt for the provided user is.
6668
* @param userId the id of the user to look for
6769
*
6870
* @return the eventId where the read receipt for the provided user is attached, or null if not found
@@ -74,4 +76,8 @@ interface ReadService {
7476
* @param eventId the event
7577
*/
7678
fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>>
79+
80+
companion object {
81+
const val THREAD_ID_MAIN = "main"
82+
}
7783
}

matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ private fun handleReadReceipts(realm: Realm, roomId: String, eventEntity: EventE
132132
val originServerTs = eventEntity.originServerTs
133133
if (originServerTs != null) {
134134
val timestampOfEvent = originServerTs.toDouble()
135-
val readReceiptOfSender = ReadReceiptEntity.getOrCreate(realm, roomId = roomId, userId = senderId)
135+
val readReceiptOfSender = ReadReceiptEntity.getOrCreate(realm, roomId = roomId, userId = senderId, threadId = eventEntity.rootThreadEventId)
136136
// If the synced RR is older, update
137137
if (timestampOfEvent > readReceiptOfSender.originServerTs) {
138138
val previousReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId = readReceiptOfSender.eventId).findFirst()

matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt

Lines changed: 28 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,11 @@ internal fun Map<String, EventEntity>.updateThreadSummaryIfNeeded(
6565
inThreadMessages = inThreadMessages,
6666
latestMessageTimelineEventEntity = latestEventInThread
6767
)
68-
}
69-
}
7068

71-
if (shouldUpdateNotifications) {
72-
updateNotificationsNew(roomId, realm, currentUserId)
69+
if (shouldUpdateNotifications) {
70+
updateThreadNotifications(roomId, realm, currentUserId, rootThreadEventId)
71+
}
72+
}
7373
}
7474
}
7575

@@ -273,8 +273,8 @@ internal fun TimelineEventEntity.Companion.isUserMentionedInThread(realm: Realm,
273273
/**
274274
* Find the read receipt for the current user.
275275
*/
276-
internal fun findMyReadReceipt(realm: Realm, roomId: String, userId: String): String? =
277-
ReadReceiptEntity.where(realm, roomId = roomId, userId = userId)
276+
internal fun findMyReadReceipt(realm: Realm, roomId: String, userId: String, threadId: String?): String? =
277+
ReadReceiptEntity.where(realm, roomId = roomId, userId = userId, threadId = threadId)
278278
.findFirst()
279279
?.eventId
280280

@@ -293,28 +293,29 @@ internal fun isUserMentioned(currentUserId: String, timelineEventEntity: Timelin
293293
* Important: It will work only with the latest chunk, while read marker will be changed
294294
* immediately so we should not display wrong notifications
295295
*/
296-
internal fun updateNotificationsNew(roomId: String, realm: Realm, currentUserId: String) {
297-
val readReceipt = findMyReadReceipt(realm, roomId, currentUserId) ?: return
296+
internal fun updateThreadNotifications(roomId: String, realm: Realm, currentUserId: String, rootThreadEventId: String) {
297+
val readReceipt = findMyReadReceipt(realm, roomId, currentUserId, threadId = rootThreadEventId) ?: return
298298

299299
val readReceiptChunk = ChunkEntity
300300
.findIncludingEvent(realm, readReceipt) ?: return
301301

302-
val readReceiptChunkTimelineEvents = readReceiptChunk
302+
val readReceiptChunkThreadEvents = readReceiptChunk
303303
.timelineEvents
304304
.where()
305305
.equalTo(TimelineEventEntityFields.ROOM_ID, roomId)
306+
.equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId)
306307
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
307308
.findAll() ?: return
308309

309-
val readReceiptChunkPosition = readReceiptChunkTimelineEvents.indexOfFirst { it.eventId == readReceipt }
310+
val readReceiptChunkPosition = readReceiptChunkThreadEvents.indexOfFirst { it.eventId == readReceipt }
310311

311312
if (readReceiptChunkPosition == -1) return
312313

313-
if (readReceiptChunkPosition < readReceiptChunkTimelineEvents.lastIndex) {
314+
if (readReceiptChunkPosition < readReceiptChunkThreadEvents.lastIndex) {
314315
// If the read receipt is found inside the chunk
315316

316-
val threadEventsAfterReadReceipt = readReceiptChunkTimelineEvents
317-
.slice(readReceiptChunkPosition..readReceiptChunkTimelineEvents.lastIndex)
317+
val threadEventsAfterReadReceipt = readReceiptChunkThreadEvents
318+
.slice(readReceiptChunkPosition..readReceiptChunkThreadEvents.lastIndex)
318319
.filter { it.root?.isThread() == true }
319320

320321
// 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:
343344
it.root?.rootThreadEventId
344345
}
345346

346-
// Find the root events in the new thread events
347-
val rootThreads = threadEventsAfterReadReceipt.distinctBy { it.root?.rootThreadEventId }.mapNotNull { it.root?.rootThreadEventId }
348-
349-
// Update root thread events only if the user have participated in
350-
rootThreads.forEach { eventId ->
351-
val isUserParticipating = TimelineEventEntity.isUserParticipatingInThread(
352-
realm = realm,
353-
roomId = roomId,
354-
rootThreadEventId = eventId,
355-
senderId = currentUserId
356-
)
357-
val rootThreadEventEntity = EventEntity.where(realm, eventId).findFirst()
358-
359-
if (isUserParticipating) {
360-
rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_MESSAGE
361-
}
347+
// Update root thread event only if the user have participated in
348+
val isUserParticipating = TimelineEventEntity.isUserParticipatingInThread(
349+
realm = realm,
350+
roomId = roomId,
351+
rootThreadEventId = rootThreadEventId,
352+
senderId = currentUserId
353+
)
354+
val rootThreadEventEntity = EventEntity.where(realm, rootThreadEventId).findFirst()
355+
356+
if (isUserParticipating) {
357+
rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_MESSAGE
358+
}
362359

363-
if (userMentionsList.contains(eventId)) {
364-
rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE
365-
}
360+
if (userMentionsList.contains(rootThreadEventId)) {
361+
rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE
366362
}
367363
}
368364
}

matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ internal class ReadReceiptsSummaryMapper @Inject constructor(
5050
.mapNotNull {
5151
val roomMember = RoomMemberSummaryEntity.where(realm, roomId = it.roomId, userId = it.userId).findFirst()
5252
?: return@mapNotNull null
53-
ReadReceipt(roomMember.asDomain(), it.originServerTs.toLong())
53+
ReadReceipt(roomMember.asDomain(), it.originServerTs.toLong(), it.threadId)
5454
}
5555
}
5656
}

matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadReceiptEntity.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ internal open class ReadReceiptEntity(
2626
var eventId: String = "",
2727
var roomId: String = "",
2828
var userId: String = "",
29+
var threadId: String? = null,
2930
var originServerTs: Double = 0.0
3031
) : RealmObject() {
3132
companion object

matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import io.realm.RealmObject
2020
import io.realm.RealmResults
2121
import io.realm.annotations.Index
2222
import io.realm.annotations.LinkingObjects
23+
import org.matrix.android.sdk.api.session.room.read.ReadService
2324
import org.matrix.android.sdk.internal.extensions.assertIsManaged
2425

2526
internal open class TimelineEventEntity(
@@ -52,3 +53,7 @@ internal fun TimelineEventEntity.deleteOnCascade(canDeleteRoot: Boolean) {
5253
}
5354
deleteFromRealm()
5455
}
56+
57+
internal fun TimelineEventEntity.getThreadId(): String {
58+
return root?.rootThreadEventId ?: ReadService.THREAD_ID_MAIN
59+
}

matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,20 @@ package org.matrix.android.sdk.internal.database.query
1818
import io.realm.Realm
1919
import io.realm.RealmConfiguration
2020
import org.matrix.android.sdk.api.session.events.model.LocalEcho
21+
import org.matrix.android.sdk.api.session.room.read.ReadService
2122
import org.matrix.android.sdk.internal.database.helper.isMoreRecentThan
2223
import org.matrix.android.sdk.internal.database.model.ChunkEntity
2324
import org.matrix.android.sdk.internal.database.model.ReadMarkerEntity
2425
import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity
2526
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
27+
import org.matrix.android.sdk.internal.database.model.getThreadId
2628

2729
internal fun isEventRead(
2830
realmConfiguration: RealmConfiguration,
2931
userId: String?,
3032
roomId: String?,
31-
eventId: String?
33+
eventId: String?,
34+
shouldCheckIfReadInEventsThread: Boolean
3235
): Boolean {
3336
if (userId.isNullOrBlank() || roomId.isNullOrBlank() || eventId.isNullOrBlank()) {
3437
return false
@@ -45,7 +48,8 @@ internal fun isEventRead(
4548
eventToCheck.root?.sender == userId -> true
4649
// If new event exists and the latest event is from ourselves we can infer the event is read
4750
latestEventIsFromSelf(realm, roomId, userId) -> true
48-
eventToCheck.isBeforeLatestReadReceipt(realm, roomId, userId) -> true
51+
eventToCheck.isBeforeLatestReadReceipt(realm, roomId, userId, null) -> true
52+
(shouldCheckIfReadInEventsThread && eventToCheck.isBeforeLatestReadReceipt(realm, roomId, userId, eventToCheck.getThreadId())) -> true
4953
else -> false
5054
}
5155
}
@@ -54,27 +58,33 @@ internal fun isEventRead(
5458
private fun latestEventIsFromSelf(realm: Realm, roomId: String, userId: String) = TimelineEventEntity.latestEvent(realm, roomId, true)
5559
?.root?.sender == userId
5660

57-
private fun TimelineEventEntity.isBeforeLatestReadReceipt(realm: Realm, roomId: String, userId: String): Boolean {
58-
return ReadReceiptEntity.where(realm, roomId, userId).findFirst()?.let { readReceipt ->
61+
private fun TimelineEventEntity.isBeforeLatestReadReceipt(realm: Realm, roomId: String, userId: String, threadId: String?): Boolean {
62+
val isMoreRecent = ReadReceiptEntity.where(realm, roomId, userId, threadId).findFirst()?.let { readReceipt ->
5963
val readReceiptEvent = TimelineEventEntity.where(realm, roomId, readReceipt.eventId).findFirst()
6064
readReceiptEvent?.isMoreRecentThan(this)
6165
} ?: false
66+
return isMoreRecent
6267
}
6368

6469
/**
6570
* Missing events can be caused by the latest timeline chunk no longer contain an older event or
6671
* by fast lane eagerly displaying events before the database has finished updating.
6772
*/
68-
private fun hasReadMissingEvent(realm: Realm, latestChunkEntity: ChunkEntity, roomId: String, userId: String, eventId: String): Boolean {
69-
return realm.doesEventExistInChunkHistory(eventId) && realm.hasReadReceiptInLatestChunk(latestChunkEntity, roomId, userId)
73+
private fun hasReadMissingEvent(realm: Realm,
74+
latestChunkEntity: ChunkEntity,
75+
roomId: String,
76+
userId: String,
77+
eventId: String,
78+
threadId: String? = ReadService.THREAD_ID_MAIN): Boolean {
79+
return realm.doesEventExistInChunkHistory(eventId) && realm.hasReadReceiptInLatestChunk(latestChunkEntity, roomId, userId, threadId)
7080
}
7181

7282
private fun Realm.doesEventExistInChunkHistory(eventId: String): Boolean {
7383
return ChunkEntity.findIncludingEvent(this, eventId) != null
7484
}
7585

76-
private fun Realm.hasReadReceiptInLatestChunk(latestChunkEntity: ChunkEntity, roomId: String, userId: String): Boolean {
77-
return ReadReceiptEntity.where(this, roomId = roomId, userId = userId).findFirst()?.let {
86+
private fun Realm.hasReadReceiptInLatestChunk(latestChunkEntity: ChunkEntity, roomId: String, userId: String, threadId: String?): Boolean {
87+
return ReadReceiptEntity.where(this, roomId = roomId, userId = userId, threadId = threadId).findFirst()?.let {
7888
latestChunkEntity.timelineEvents.find(it.eventId)
7989
} != null
8090
}

0 commit comments

Comments
 (0)