diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReadReceipt.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReadReceipt.kt index 0516b1af..e168dc1e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReadReceipt.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReadReceipt.kt @@ -16,8 +16,9 @@ package im.vector.matrix.android.api.session.room.model +import im.vector.matrix.android.api.session.user.model.User + data class ReadReceipt( - val userId: String, - val eventId: String, + val user: User, val originServerTs: Long ) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt index 5d04d2f5..36ca360e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt @@ -20,6 +20,7 @@ import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary +import im.vector.matrix.android.api.session.room.model.ReadReceipt import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.isReply import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply @@ -37,7 +38,8 @@ data class TimelineEvent( val senderName: String?, val isUniqueDisplayName: Boolean, val senderAvatar: String?, - val annotations: EventAnnotationsSummary? = null + val annotations: EventAnnotationsSummary? = null, + val readReceipts: List = emptyList() ) { val metadata = HashMap() @@ -65,8 +67,8 @@ data class TimelineEvent( "$name (${root.senderId})" } } - ?: root.senderId - ?: "" + ?: root.senderId + ?: "" } /** @@ -94,7 +96,7 @@ fun TimelineEvent.hasBeenEdited() = annotations?.editSummary != null * Get last MessageContent, after a possible edition */ fun TimelineEvent.getLastMessageContent(): MessageContent? = annotations?.editSummary?.aggregatedContent?.toModel() - ?: root.getClearContent().toModel() + ?: root.getClearContent().toModel() fun TimelineEvent.getTextEditableContent(): String? { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt index 3bda568d..5a76741e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt @@ -23,6 +23,8 @@ import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.mapper.toEntity import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity +import im.vector.matrix.android.internal.database.model.ReadReceiptEntity +import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields import im.vector.matrix.android.internal.database.query.find @@ -133,6 +135,23 @@ internal fun ChunkEntity.add(roomId: String, } val localId = TimelineEventEntity.nextId(realm) + val eventId = event.eventId ?: "" + val senderId = event.senderId ?: "" + + val currentReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst() + ?: ReadReceiptsSummaryEntity(eventId) + + // Update RR for the sender of a new message + if (direction == PaginationDirection.FORWARDS && !isUnlinked) { + ReadReceiptEntity.where(realm, roomId = roomId, userId = senderId).findFirst()?.also { + val previousEventId = it.eventId + val previousReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId = previousEventId).findFirst() + it.eventId = eventId + previousReceiptsSummary?.readReceipts?.remove(it) + currentReceiptsSummary.readReceipts.add(it) + } + } + val eventEntity = TimelineEventEntity(localId).also { it.root = event.toEntity(roomId).apply { this.stateIndex = currentStateIndex @@ -140,9 +159,10 @@ internal fun ChunkEntity.add(roomId: String, this.displayIndex = currentDisplayIndex this.sendState = SendState.SYNCED } - it.eventId = event.eventId ?: "" + it.eventId = eventId it.roomId = roomId - it.annotations = EventAnnotationsSummaryEntity.where(realm, it.eventId).findFirst() + it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() + it.readReceipts = currentReceiptsSummary } val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size timelineEvents.add(position, eventEntity) @@ -150,14 +170,14 @@ internal fun ChunkEntity.add(roomId: String, internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { return when (direction) { - PaginationDirection.FORWARDS -> forwardsDisplayIndex - PaginationDirection.BACKWARDS -> backwardsDisplayIndex - } ?: defaultValue + PaginationDirection.FORWARDS -> forwardsDisplayIndex + PaginationDirection.BACKWARDS -> backwardsDisplayIndex + } ?: defaultValue } internal fun ChunkEntity.lastStateIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { return when (direction) { - PaginationDirection.FORWARDS -> forwardsStateIndex - PaginationDirection.BACKWARDS -> backwardsStateIndex - } ?: defaultValue + PaginationDirection.FORWARDS -> forwardsStateIndex + PaginationDirection.BACKWARDS -> backwardsStateIndex + } ?: defaultValue } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/ReadReceiptsSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/ReadReceiptsSummaryMapper.kt new file mode 100644 index 00000000..3887f3ac --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/ReadReceiptsSummaryMapper.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.database.mapper + +import im.vector.matrix.android.api.session.room.model.ReadReceipt +import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity +import im.vector.matrix.android.internal.database.model.UserEntity +import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.di.SessionDatabase +import io.realm.Realm +import io.realm.RealmConfiguration +import javax.inject.Inject + +internal class ReadReceiptsSummaryMapper @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration){ + + fun map(readReceiptsSummaryEntity: ReadReceiptsSummaryEntity): List { + return Realm.getInstance(realmConfiguration).use { realm -> + readReceiptsSummaryEntity.readReceipts.mapNotNull { + val user = UserEntity.where(realm, it.userId).findFirst() + ?: return@mapNotNull null + ReadReceipt(user.asDomain(), it.originServerTs.toLong()) + } + } + } + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt index 45328992..95d4d8bc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt @@ -26,7 +26,8 @@ import java.util.* import javax.inject.Inject internal class RoomSummaryMapper @Inject constructor( - val cryptoService: CryptoService + val cryptoService: CryptoService, + val timelineEventMapper: TimelineEventMapper ) { fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary { @@ -34,7 +35,9 @@ internal class RoomSummaryMapper @Inject constructor( RoomTag(it.tagName, it.tagOrder) } - val latestEvent = roomSummaryEntity.latestEvent?.asDomain() + val latestEvent = roomSummaryEntity.latestEvent?.let { + timelineEventMapper.map(it) + } if (latestEvent?.root?.isEncrypted() == true && latestEvent.root.mxDecryptionResult == null) { //TODO use a global event decryptor? attache to session and that listen to new sessionId? //for now decrypt sync diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt index 61d5a601..5290692c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt @@ -17,29 +17,30 @@ package im.vector.matrix.android.internal.database.mapper import im.vector.matrix.android.api.session.events.model.Event + import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.internal.database.model.TimelineEventEntity +import javax.inject.Inject -internal object TimelineEventMapper { +internal class TimelineEventMapper @Inject constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper){ fun map(timelineEventEntity: TimelineEventEntity): TimelineEvent { return TimelineEvent( root = timelineEventEntity.root?.asDomain() - ?: Event("", timelineEventEntity.eventId), + ?: Event("", timelineEventEntity.eventId), annotations = timelineEventEntity.annotations?.asDomain(), localId = timelineEventEntity.localId, displayIndex = timelineEventEntity.root?.displayIndex ?: 0, senderName = timelineEventEntity.senderName, isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName, - senderAvatar = timelineEventEntity.senderAvatar + senderAvatar = timelineEventEntity.senderAvatar, + readReceipts = timelineEventEntity.readReceipts?.let { + readReceiptsSummaryMapper.map(it) + } ?: emptyList() ) } } -internal fun TimelineEventEntity.asDomain(): TimelineEvent { - return TimelineEventMapper.map(this) -} - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadReceiptEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadReceiptEntity.kt index d702fdc6..b0e6a440 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadReceiptEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadReceiptEntity.kt @@ -17,13 +17,18 @@ package im.vector.matrix.android.internal.database.model import io.realm.RealmObject +import io.realm.RealmResults +import io.realm.annotations.LinkingObjects import io.realm.annotations.PrimaryKey internal open class ReadReceiptEntity(@PrimaryKey var primaryKey: String = "", - var userId: String = "", - var eventId: String = "", - var roomId: String = "", - var originServerTs: Double = 0.0 + var eventId: String = "", + var roomId: String = "", + var userId: String = "", + var originServerTs: Double = 0.0 ) : RealmObject() { companion object + + @LinkingObjects("readReceipts") + val summary: RealmResults? = null } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadReceiptsSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadReceiptsSummaryEntity.kt new file mode 100644 index 00000000..e0fe970f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadReceiptsSummaryEntity.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.database.model + +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class ReadReceiptsSummaryEntity( + @PrimaryKey + var eventId: String = "", + var readReceipts: RealmList = RealmList() +) : RealmObject() { + + companion object + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt index 0e4dc1ae..1d27bf07 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt @@ -22,26 +22,27 @@ import io.realm.annotations.RealmModule * Realm module for Session */ @RealmModule(library = true, - classes = [ - ChunkEntity::class, - EventEntity::class, - TimelineEventEntity::class, - FilterEntity::class, - GroupEntity::class, - GroupSummaryEntity::class, - ReadReceiptEntity::class, - RoomEntity::class, - RoomSummaryEntity::class, - RoomTagEntity::class, - SyncEntity::class, - UserEntity::class, - EventAnnotationsSummaryEntity::class, - ReactionAggregatedSummaryEntity::class, - EditAggregatedSummaryEntity::class, - PushRulesEntity::class, - PushRuleEntity::class, - PushConditionEntity::class, - PusherEntity::class, - PusherDataEntity::class - ]) + classes = [ + ChunkEntity::class, + EventEntity::class, + TimelineEventEntity::class, + FilterEntity::class, + GroupEntity::class, + GroupSummaryEntity::class, + ReadReceiptEntity::class, + RoomEntity::class, + RoomSummaryEntity::class, + RoomTagEntity::class, + SyncEntity::class, + UserEntity::class, + EventAnnotationsSummaryEntity::class, + ReactionAggregatedSummaryEntity::class, + EditAggregatedSummaryEntity::class, + PushRulesEntity::class, + PushRuleEntity::class, + PushConditionEntity::class, + PusherEntity::class, + PusherDataEntity::class, + ReadReceiptsSummaryEntity::class + ]) internal class SessionRealmModule diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt index a1e58c90..429b2291 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt @@ -30,7 +30,8 @@ internal open class TimelineEventEntity(var localId: Long = 0, var senderName: String? = null, var isUniqueDisplayName: Boolean = false, var senderAvatar: String? = null, - var senderMembershipEvent: EventEntity? = null + var senderMembershipEvent: EventEntity? = null, + var readReceipts: ReadReceiptsSummaryEntity? = null ) : RealmObject() { @LinkingObjects("timelineEvents") diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadReceiptsSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadReceiptsSummaryEntityQueries.kt new file mode 100644 index 00000000..e6c1e685 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadReceiptsSummaryEntityQueries.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.database.query + +import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity +import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where + +internal fun ReadReceiptsSummaryEntity.Companion.where(realm: Realm, eventId: String): RealmQuery { + return realm.where() + .equalTo(ReadReceiptsSummaryEntityFields.EVENT_ID, eventId) +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt index 1c64c91b..dd5d2d3b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt @@ -22,6 +22,7 @@ import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper +import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask @@ -47,6 +48,7 @@ internal class RoomFactory @Inject constructor(private val context: Context, private val monarchy: Monarchy, private val eventFactory: LocalEchoEventFactory, private val roomSummaryMapper: RoomSummaryMapper, + private val timelineEventMapper: TimelineEventMapper, private val taskExecutor: TaskExecutor, private val loadRoomMembersTask: LoadRoomMembersTask, private val inviteTask: InviteTask, @@ -61,13 +63,13 @@ internal class RoomFactory @Inject constructor(private val context: Context, private val leaveRoomTask: LeaveRoomTask) { fun create(roomId: String): Room { - val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, cryptoService, paginationTask) + val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, cryptoService, paginationTask, timelineEventMapper) val sendService = DefaultSendService(context, credentials, roomId, eventFactory, cryptoService, monarchy) val stateService = DefaultStateService(roomId, monarchy.realmConfiguration, taskExecutor, sendStateTask) val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask) val readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask, credentials) val relationService = DefaultRelationService(context, - credentials, roomId, eventFactory, cryptoService, findReactionEventForUndoTask, fetchEditHistoryTask, monarchy, taskExecutor) + credentials, roomId, eventFactory, cryptoService, findReactionEventForUndoTask, fetchEditHistoryTask, monarchy, taskExecutor) return DefaultRoom( roomId, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index 10f4874f..921c65ea 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -23,6 +23,7 @@ import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.util.CancelableBag +import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.* import im.vector.matrix.android.internal.database.model.EventEntity @@ -53,7 +54,8 @@ internal class DefaultTimeline( private val taskExecutor: TaskExecutor, private val contextOfEventTask: GetContextOfEventTask, private val paginationTask: PaginationTask, - cryptoService: CryptoService, + private val cryptoService: CryptoService, + private val timelineEventMapper: TimelineEventMapper, private val allowedTypes: List? ) : Timeline { @@ -132,7 +134,7 @@ internal class DefaultTimeline( builtEventsIdMap[eventId]?.let { builtIndex -> //Update the relation of existing event builtEvents[builtIndex]?.let { te -> - builtEvents[builtIndex] = eventEntity.asDomain() + builtEvents[builtIndex] = timelineEventMapper.map(eventEntity) hasChanged = true } } @@ -331,7 +333,7 @@ internal class DefaultTimeline( roomEntity?.sendingTimelineEvents ?.filter { allowedTypes?.contains(it.root?.type) ?: false } ?.forEach { - sendingEvents.add(it.asDomain()) + sendingEvents.add(timelineEventMapper.map(it)) } } return sendingEvents @@ -463,7 +465,7 @@ internal class DefaultTimeline( nextDisplayIndex = offsetIndex + 1 } offsetResults.forEach { eventEntity -> - val timelineEvent = eventEntity.asDomain() + val timelineEvent = timelineEventMapper.map(eventEntity) if (timelineEvent.isEncrypted() && timelineEvent.root.mxDecryptionResult == null) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt index d70f1b92..94fc433a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt @@ -24,6 +24,7 @@ import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineService import im.vector.matrix.android.internal.database.RealmLiveData +import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.query.where @@ -36,7 +37,8 @@ internal class DefaultTimelineService @Inject constructor(private val roomId: St private val taskExecutor: TaskExecutor, private val contextOfEventTask: GetContextOfEventTask, private val cryptoService: CryptoService, - private val paginationTask: PaginationTask + private val paginationTask: PaginationTask, + private val timelineEventMapper: TimelineEventMapper ) : TimelineService { override fun createTimeline(eventId: String?, allowedTypes: List?): Timeline { @@ -47,7 +49,9 @@ internal class DefaultTimelineService @Inject constructor(private val roomId: St contextOfEventTask, paginationTask, cryptoService, - allowedTypes) + timelineEventMapper, + allowedTypes + ) } override fun getTimeLineEvent(eventId: String): TimelineEvent? { @@ -55,7 +59,7 @@ internal class DefaultTimelineService @Inject constructor(private val roomId: St .fetchCopyMap({ TimelineEventEntity.where(it, eventId = eventId).findFirst() }, { entity, realm -> - entity.asDomain() + timelineEventMapper.map(entity) }) } @@ -63,8 +67,8 @@ internal class DefaultTimelineService @Inject constructor(private val roomId: St val liveData = RealmLiveData(monarchy.realmConfiguration) { TimelineEventEntity.where(it, eventId = eventId) } - return Transformations.map(liveData) { - it.firstOrNull()?.asDomain() + return Transformations.map(liveData) { events -> + events.firstOrNull()?.let { timelineEventMapper.map(it) } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/ReadReceiptHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/ReadReceiptHandler.kt index 9ada6e71..055334b1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/ReadReceiptHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/ReadReceiptHandler.kt @@ -17,6 +17,8 @@ package im.vector.matrix.android.internal.session.sync import im.vector.matrix.android.internal.database.model.ReadReceiptEntity +import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity +import im.vector.matrix.android.internal.database.query.where import io.realm.Realm import timber.log.Timber import javax.inject.Inject @@ -36,27 +38,35 @@ internal class ReadReceiptHandler @Inject constructor() { return } try { - val readReceipts = mapContentToReadReceiptEntities(roomId, content) - realm.insertOrUpdate(readReceipts) + handleReadReceiptContent(realm, roomId, content) } catch (exception: Exception) { Timber.e("Fail to handle read receipt for room $roomId") } } - private fun mapContentToReadReceiptEntities(roomId: String, content: ReadReceiptContent): List { - return content - .flatMap { (eventId, receiptDict) -> - receiptDict - .filterKeys { it == "m.read" } - .flatMap { (_, userIdsDict) -> - userIdsDict.map { (userId, paramsDict) -> - val ts = paramsDict.filterKeys { it == "ts" } - .values - .firstOrNull() ?: 0.0 - val primaryKey = roomId + userId - ReadReceiptEntity(primaryKey, userId, eventId, roomId, ts) - } - } + private fun handleReadReceiptContent(realm: Realm, roomId: String, content: ReadReceiptContent) { + for ((eventId, receiptDict) in content) { + val userIdsDict = receiptDict["m.read"] ?: continue + val readReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst() + ?: realm.createObject(ReadReceiptsSummaryEntity::class.java, eventId) + + for ((userId, paramsDict) in userIdsDict) { + val ts = paramsDict["ts"] ?: 0.0 + val primaryKey = "${roomId}_$userId" + val receiptEntity = ReadReceiptEntity.where(realm, roomId, userId).findFirst() + ?: realm.createObject(ReadReceiptEntity::class.java, primaryKey) + + ReadReceiptsSummaryEntity.where(realm, receiptEntity.eventId).findFirst()?.also { + it.readReceipts.remove(receiptEntity) } + receiptEntity.apply { + this.eventId = eventId + this.roomId = roomId + this.userId = userId + this.originServerTs = ts + } + readReceiptsSummary.readReceipts.add(receiptEntity) + } + } } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt index 9da3db76..a16cae18 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt @@ -117,7 +117,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch Timber.v("Handle join sync for room $roomId") val roomEntity = RoomEntity.where(realm, roomId).findFirst() - ?: realm.createObject(roomId) + ?: realm.createObject(roomId) if (roomEntity.membership == Membership.INVITE) { roomEntity.chunks.deleteAllFromRealm() @@ -127,7 +127,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch // State event if (roomSync.state != null && roomSync.state.events.isNotEmpty()) { val minStateIndex = roomEntity.untimelinedStateEvents.where().min(EventEntityFields.STATE_INDEX)?.toInt() - ?: Int.MIN_VALUE + ?: Int.MIN_VALUE val untimelinedStateIndex = minStateIndex + 1 roomSync.state.events.forEach { event -> roomEntity.addStateEvent(event, filterDuplicates = true, stateIndex = untimelinedStateIndex) @@ -167,7 +167,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch InvitedRoomSync): RoomEntity { Timber.v("Handle invited sync for room $roomId") val roomEntity = RoomEntity.where(realm, roomId).findFirst() - ?: realm.createObject(roomId) + ?: realm.createObject(roomId) roomEntity.membership = Membership.INVITE if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) { val chunkEntity = handleTimelineEvents(realm, roomEntity, roomSync.inviteState.events) @@ -181,7 +181,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch roomId: String, roomSync: RoomSync): RoomEntity { val roomEntity = RoomEntity.where(realm, roomId).findFirst() - ?: realm.createObject(roomId) + ?: realm.createObject(roomId) roomEntity.membership = Membership.LEAVE roomEntity.chunks.deleteAllFromRealm() @@ -233,17 +233,20 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch } + @Suppress("UNCHECKED_CAST") private fun handleEphemeral(realm: Realm, roomId: String, ephemeral: RoomSyncEphemeral) { - ephemeral.events - .filter { it.getClearType() == EventType.RECEIPT } - .map { it.content.toModel() } - .forEach { readReceiptHandler.handle(realm, roomId, it) } + for (event in ephemeral.events) { + if (event.type != EventType.RECEIPT) continue + val readReceiptContent = event.content as? ReadReceiptContent ?: continue + readReceiptHandler.handle(realm, roomId, readReceiptContent) + } } private fun handleRoomAccountDataEvents(realm: Realm, roomId: String, accountData: RoomSyncAccountData) { accountData.events + .asSequence() .filter { it.getClearType() == EventType.TAG } .map { it.content.toModel() } .forEach { roomTagHandler.handle(realm, roomId, it) } diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt index 6145d5a7..1214bfa0 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt @@ -161,7 +161,6 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector { override fun onDestroy() { super.onDestroy() - unBinder?.unbind() unBinder = null diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index f81ef9d3..5ed7bcb3 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -28,8 +28,15 @@ import im.vector.matrix.android.api.permalinks.MatrixLinkify import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan import im.vector.matrix.android.api.session.events.model.RelationType import im.vector.matrix.android.api.session.events.model.toModel -import im.vector.matrix.android.api.session.room.model.message.* -import im.vector.matrix.android.api.session.room.send.SendState +import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent +import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageEmoteContent +import im.vector.matrix.android.api.session.room.model.message.MessageFileContent +import im.vector.matrix.android.api.session.room.model.message.MessageImageContent +import im.vector.matrix.android.api.session.room.model.message.MessageNoticeContent +import im.vector.matrix.android.api.session.room.model.message.MessageTextContent +import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent +import im.vector.matrix.android.api.session.room.model.message.getFileUrl import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt @@ -47,7 +54,19 @@ import im.vector.riotx.features.home.room.detail.timeline.TimelineEventControlle import im.vector.riotx.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider import im.vector.riotx.features.home.room.detail.timeline.helper.senderAvatar -import im.vector.riotx.features.home.room.detail.timeline.item.* +import im.vector.riotx.features.home.room.detail.timeline.item.BlankItem_ +import im.vector.riotx.features.home.room.detail.timeline.item.DefaultItem +import im.vector.riotx.features.home.room.detail.timeline.item.DefaultItem_ +import im.vector.riotx.features.home.room.detail.timeline.item.MessageFileItem +import im.vector.riotx.features.home.room.detail.timeline.item.MessageFileItem_ +import im.vector.riotx.features.home.room.detail.timeline.item.MessageImageVideoItem +import im.vector.riotx.features.home.room.detail.timeline.item.MessageImageVideoItem_ +import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData +import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem +import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem_ +import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem_ +import im.vector.riotx.features.home.room.detail.timeline.item.RedactedMessageItem +import im.vector.riotx.features.home.room.detail.timeline.item.RedactedMessageItem_ import im.vector.riotx.features.home.room.detail.timeline.util.MessageInformationDataFactory import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.media.ImageContentRenderer @@ -84,11 +103,11 @@ class MessageItemFactory @Inject constructor( val messageContent: MessageContent = event.getLastMessageContent() - ?: //Malformed content, we should echo something on screen - return DefaultItem_().text(stringProvider.getString(R.string.malformed_message)) + ?: //Malformed content, we should echo something on screen + return DefaultItem_().text(stringProvider.getString(R.string.malformed_message)) if (messageContent.relatesTo?.type == RelationType.REPLACE - || event.isEncrypted() && event.root.content.toModel()?.relatesTo?.type == RelationType.REPLACE + || event.isEncrypted() && event.root.content.toModel()?.relatesTo?.type == RelationType.REPLACE ) { // ignore replace event, the targeted id is already edited if (userPreferencesProvider.shouldShowHiddenEvents()) { @@ -116,15 +135,13 @@ class MessageItemFactory @Inject constructor( // val ev = all.toModel() return when (messageContent) { is MessageEmoteContent -> buildEmoteMessageItem(messageContent, - informationData, - highlight, - callback) - is MessageTextContent -> buildTextMessageItem(event.root.sendState, - messageContent, - informationData, - highlight, - callback - ) + informationData, + highlight, + callback) + is MessageTextContent -> buildTextMessageItem(messageContent, + informationData, + highlight, + callback) is MessageImageContent -> buildImageMessageItem(messageContent, informationData, highlight, callback) is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback) is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback) @@ -158,7 +175,7 @@ class MessageItemFactory @Inject constructor( })) .longClickListener { view -> return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false + ?: false } } @@ -182,7 +199,7 @@ class MessageItemFactory @Inject constructor( })) .longClickListener { view -> return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false + ?: false } .clickListener( DebouncedClickListener(View.OnClickListener { _ -> @@ -190,7 +207,7 @@ class MessageItemFactory @Inject constructor( })) .longClickListener { view -> return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false + ?: false } } @@ -240,7 +257,7 @@ class MessageItemFactory @Inject constructor( })) .longClickListener { view -> return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false + ?: false } } @@ -253,7 +270,7 @@ class MessageItemFactory @Inject constructor( val thumbnailData = ImageContentRenderer.Data( filename = messageContent.body, url = messageContent.videoInfo?.thumbnailFile?.url - ?: messageContent.videoInfo?.thumbnailUrl, + ?: messageContent.videoInfo?.thumbnailUrl, elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(), height = messageContent.videoInfo?.height, maxHeight = maxHeight, @@ -288,12 +305,11 @@ class MessageItemFactory @Inject constructor( .clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) } .longClickListener { view -> return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false + ?: false } } - private fun buildTextMessageItem(sendState: SendState, - messageContent: MessageTextContent, + private fun buildTextMessageItem(messageContent: MessageTextContent, informationData: MessageInformationData, highlight: Boolean, callback: TimelineEventController.Callback?): MessageTextItem? { @@ -328,7 +344,7 @@ class MessageItemFactory @Inject constructor( })) .longClickListener { view -> return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false + ?: false } } @@ -358,9 +374,9 @@ class MessageItemFactory @Inject constructor( //nop } }, - editStart, - editEnd, - Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + editStart, + editEnd, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE) return spannable } @@ -397,7 +413,7 @@ class MessageItemFactory @Inject constructor( })) .longClickListener { view -> return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false + ?: false } } @@ -433,7 +449,7 @@ class MessageItemFactory @Inject constructor( })) .longClickListener { view -> return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false + ?: false } } @@ -452,7 +468,7 @@ class MessageItemFactory @Inject constructor( })) .longClickListener { view -> return@longClickListener callback?.onEventLongClicked(informationData, null, view) - ?: false + ?: false } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt index 670cf471..1462e842 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -16,6 +16,7 @@ package im.vector.riotx.features.home.room.detail.timeline.item +import android.annotation.SuppressLint import android.graphics.Typeface import android.os.Build import android.view.View @@ -39,6 +40,7 @@ import im.vector.riotx.features.home.room.detail.timeline.TimelineEventControlle import im.vector.riotx.features.reactions.widget.ReactionButton import im.vector.riotx.features.ui.getMessageTextColor +private const val MAX_RECEIPT_DISPLAYED = 5 abstract class AbsMessageItem : BaseEventItem() { @@ -123,6 +125,29 @@ abstract class AbsMessageItem : BaseEventItem() { holder.memberNameView.setOnLongClickListener(null) } + if (informationData.readReceipts.isNotEmpty()) { + holder.readReceiptsView.isVisible = true + for (index in 0 until MAX_RECEIPT_DISPLAYED) { + val receiptData = informationData.readReceipts.getOrNull(index) + if (receiptData == null) { + holder.receiptAvatars[index].isVisible = false + } else { + holder.receiptAvatars[index].isVisible = true + avatarRenderer.render(receiptData.avatarUrl, receiptData.userId, receiptData.displayName, holder.receiptAvatars[index]) + } + } + if (informationData.readReceipts.size > MAX_RECEIPT_DISPLAYED) { + holder.receiptMoreView.isVisible = true + holder.receiptMoreView.text = holder.view.context.getString( + R.string.x_plus, informationData.readReceipts.size - MAX_RECEIPT_DISPLAYED + ) + } else { + holder.receiptMoreView.isVisible = false + } + } else { + holder.readReceiptsView.isVisible = false + } + if (!shouldShowReactionAtBottom() || informationData.orderedReactionList.isNullOrEmpty()) { holder.reactionWrapper?.isVisible = false } else { @@ -173,6 +198,16 @@ abstract class AbsMessageItem : BaseEventItem() { val avatarImageView by bind(R.id.messageAvatarImageView) val memberNameView by bind(R.id.messageMemberNameView) val timeView by bind(R.id.messageTimeView) + val readReceiptsView by bind(R.id.readReceiptsView) + val receiptAvatar1 by bind(R.id.message_avatar_receipt_1) + val receiptAvatar2 by bind(R.id.message_avatar_receipt_2) + val receiptAvatar3 by bind(R.id.message_avatar_receipt_3) + val receiptAvatar4 by bind(R.id.message_avatar_receipt_4) + val receiptAvatar5 by bind(R.id.message_avatar_receipt_5) + val receiptMoreView by bind(R.id.message_more_than_expected) + val receiptAvatars: List by lazy { + listOf(receiptAvatar1, receiptAvatar2, receiptAvatar3, receiptAvatar4, receiptAvatar5) + } var reactionWrapper: ViewGroup? = null var reactionFlowHelper: Flow? = null diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt index 31b92c0e..679bfbba 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt @@ -17,6 +17,7 @@ package im.vector.riotx.features.home.room.detail.timeline.item import android.os.Parcelable +import im.vector.matrix.android.api.session.room.model.ReadReceipt import im.vector.matrix.android.api.session.room.send.SendState import kotlinx.android.parcel.Parcelize @@ -32,7 +33,8 @@ data class MessageInformationData( /*List of reactions (emoji,count,isSelected)*/ val orderedReactionList: List? = null, val hasBeenEdited: Boolean = false, - val hasPendingEdits: Boolean = false + val hasPendingEdits: Boolean = false, + val readReceipts: List = emptyList() ) : Parcelable @@ -43,3 +45,11 @@ data class ReactionInfoData( val addedByMe: Boolean, val synced: Boolean ) : Parcelable + +@Parcelize +data class ReadReceiptData( + val userId: String, + val avatarUrl: String?, + val displayName: String?, + val timestamp: Long +) : Parcelable \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/util/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/util/MessageInformationDataFactory.kt index fe15c5d2..2f6a4320 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/util/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/util/MessageInformationDataFactory.kt @@ -16,6 +16,7 @@ package im.vector.riotx.features.home.room.detail.timeline.util +import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited @@ -26,13 +27,15 @@ import im.vector.riotx.features.home.getColorFromUserId import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDateFormatter import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.home.room.detail.timeline.item.ReactionInfoData +import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData import me.gujun.android.span.span import javax.inject.Inject /** * This class compute if data of an event (such has avatar, display name, ...) should be displayed, depending on the previous event in the timeline */ -class MessageInformationDataFactory @Inject constructor(private val timelineDateFormatter: TimelineDateFormatter, +class MessageInformationDataFactory @Inject constructor(private val session: Session, + private val timelineDateFormatter: TimelineDateFormatter, private val colorProvider: ColorProvider) { fun create(event: TimelineEvent, nextEvent: TimelineEvent?): MessageInformationData { @@ -43,21 +46,21 @@ class MessageInformationDataFactory @Inject constructor(private val timelineDate val nextDate = nextEvent?.root?.localDateTime() val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate() val isNextMessageReceivedMoreThanOneHourAgo = nextDate?.isBefore(date.minusMinutes(60)) - ?: false + ?: false val showInformation = addDaySeparator - || event.senderAvatar != nextEvent?.senderAvatar - || event.getDisambiguatedDisplayName() != nextEvent?.getDisambiguatedDisplayName() - || (nextEvent?.root?.getClearType() != EventType.MESSAGE && nextEvent?.root?.getClearType() != EventType.ENCRYPTED) - || isNextMessageReceivedMoreThanOneHourAgo + || event.senderAvatar != nextEvent?.senderAvatar + || event.getDisambiguatedDisplayName() != nextEvent?.getDisambiguatedDisplayName() + || (nextEvent?.root?.getClearType() != EventType.MESSAGE && nextEvent?.root?.getClearType() != EventType.ENCRYPTED) + || isNextMessageReceivedMoreThanOneHourAgo val time = timelineDateFormatter.formatMessageHour(date) val avatarUrl = event.senderAvatar val memberName = event.getDisambiguatedDisplayName() val formattedMemberName = span(memberName) { textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId - ?: "")) + ?: "")) } return MessageInformationData( @@ -74,7 +77,14 @@ class MessageInformationDataFactory @Inject constructor(private val timelineDate ReactionInfoData(it.key, it.count, it.addedByMe, it.localEchoEvents.isEmpty()) }, hasBeenEdited = event.hasBeenEdited(), - hasPendingEdits = event.annotations?.editSummary?.localEchos?.any() ?: false + hasPendingEdits = event.annotations?.editSummary?.localEchos?.any() ?: false, + readReceipts = event.readReceipts + .filter { + it.user.userId != session.myUserId + } + .map { + ReadReceiptData(it.user.userId, it.user.avatarUrl, it.user.displayName, it.originServerTs) + } ) } } \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml index 7f15c60b..a8b81606 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -114,7 +114,7 @@ android:inflatedId="@+id/messageBottomInfo" android:layout="@layout/item_timeline_event_bottom_reactions_stub" android:visibility="gone" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toTopOf="@+id/readReceiptsView" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/messageStartGuideline" app:layout_constraintVertical_chainStyle="packed" @@ -123,4 +123,65 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/view_read_receipts.xml b/vector/src/main/res/layout/view_read_receipts.xml new file mode 100644 index 00000000..4f65a82a --- /dev/null +++ b/vector/src/main/res/layout/view_read_receipts.xml @@ -0,0 +1,11 @@ + + + + + + +