mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-02-01 20:51:11 +03:00
added read receipts for threads (#7474)
This commit is contained in:
parent
27419f0d33
commit
18bcc83a46
22 changed files with 214 additions and 93 deletions
1
changelog.d/6996.sdk
Normal file
1
changelog.d/6996.sdk
Normal 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)
|
|
@ -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>> {
|
||||||
|
|
|
@ -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?
|
||||||
)
|
)
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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 }
|
val isUserParticipating = TimelineEventEntity.isUserParticipatingInThread(
|
||||||
|
realm = realm,
|
||||||
|
roomId = roomId,
|
||||||
|
rootThreadEventId = rootThreadEventId,
|
||||||
|
senderId = currentUserId
|
||||||
|
)
|
||||||
|
val rootThreadEventEntity = EventEntity.where(realm, rootThreadEventId).findFirst()
|
||||||
|
|
||||||
// Update root thread events only if the user have participated in
|
if (isUserParticipating) {
|
||||||
rootThreads.forEach { eventId ->
|
rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_MESSAGE
|
||||||
val isUserParticipating = TimelineEventEntity.isUserParticipatingInThread(
|
}
|
||||||
realm = realm,
|
|
||||||
roomId = roomId,
|
|
||||||
rootThreadEventId = eventId,
|
|
||||||
senderId = currentUserId
|
|
||||||
)
|
|
||||||
val rootThreadEventEntity = EventEntity.where(realm, eventId).findFirst()
|
|
||||||
|
|
||||||
if (isUserParticipating) {
|
if (userMentionsList.contains(rootThreadEventId)) {
|
||||||
rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_MESSAGE
|
rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE
|
||||||
}
|
|
||||||
|
|
||||||
if (userMentionsList.contains(eventId)) {
|
|
||||||
rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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?,
|
||||||
|
)
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue