From b4ce8748cb3aff6f06bfcba37e4a34e0d144307a Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 8 Aug 2019 14:32:11 +0200 Subject: [PATCH 01/14] First step in handling read receipts --- .../api/session/room/model/ReadReceipt.kt | 5 +- .../session/room/timeline/TimelineEvent.kt | 10 ++- .../database/helper/ChunkEntityHelper.kt | 36 +++++++-- .../mapper/ReadReceiptsSummaryMapper.kt | 40 ++++++++++ .../database/mapper/RoomSummaryMapper.kt | 7 +- .../database/mapper/TimelineEventMapper.kt | 15 ++-- .../database/model/ReadReceiptEntity.kt | 13 +++- .../model/ReadReceiptsSummaryEntity.kt | 31 ++++++++ .../database/model/SessionRealmModule.kt | 45 +++++------ .../database/model/TimelineEventEntity.kt | 3 +- .../query/ReadReceiptsSummaryEntityQueries.kt | 28 +++++++ .../internal/session/room/RoomFactory.kt | 6 +- .../session/room/timeline/DefaultTimeline.kt | 10 ++- .../room/timeline/DefaultTimelineService.kt | 14 ++-- .../session/sync/ReadReceiptHandler.kt | 42 ++++++---- .../internal/session/sync/RoomSyncHandler.kt | 19 +++-- .../riotx/core/platform/VectorBaseActivity.kt | 1 - .../timeline/factory/MessageItemFactory.kt | 76 +++++++++++-------- .../detail/timeline/item/AbsMessageItem.kt | 35 +++++++++ .../timeline/item/MessageInformationData.kt | 12 ++- .../util/MessageInformationDataFactory.kt | 26 +++++-- .../res/layout/item_timeline_event_base.xml | 63 ++++++++++++++- .../main/res/layout/view_read_receipts.xml | 11 +++ 23 files changed, 422 insertions(+), 126 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/ReadReceiptsSummaryMapper.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadReceiptsSummaryEntity.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadReceiptsSummaryEntityQueries.kt create mode 100644 vector/src/main/res/layout/view_read_receipts.xml 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 0516b1af40..e168dc1e5b 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 5d04d2f52e..36ca360e08 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 3bda568d3a..5a76741ede 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 0000000000..3887f3aca9 --- /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 45328992e7..95d4d8bc62 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 61d5a601cd..5290692c3e 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 d702fdc669..b0e6a440eb 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 0000000000..e0fe970fe9 --- /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 0e4dc1ae43..1d27bf07ee 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 a1e58c9029..429b2291f6 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 0000000000..e6c1e68552 --- /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 1c64c91b1d..dd5d2d3b97 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 10f4874fbd..921c65eabe 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 d70f1b9232..94fc433a1e 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 9ada6e71f2..055334b15a 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 9da3db768d..a16cae1861 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 6145d5a76c..1214bfa045 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 f81ef9d333..5ed7bcb3af 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 670cf471a2..1462e842d9 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 31b92c0e0e..679bfbba6a 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 fe15c5d281..2f6a432004 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 7f15c60bfc..a8b81606e1 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 0000000000..4f65a82a60 --- /dev/null +++ b/vector/src/main/res/layout/view_read_receipts.xml @@ -0,0 +1,11 @@ + + + + + + + From d98567045cea85a4a55d334729a108bdde88cc29 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 8 Aug 2019 15:03:36 +0200 Subject: [PATCH 02/14] Read receipts: use a simpler strategy when it's initialSync --- .../session/sync/ReadReceiptHandler.kt | 46 +++++++++++++++++-- .../internal/session/sync/RoomSyncHandler.kt | 22 +++++---- .../session/sync/SyncResponseHandler.kt | 2 +- 3 files changed, 54 insertions(+), 16 deletions(-) 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 055334b15a..35acb527d5 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 @@ -31,27 +31,62 @@ import javax.inject.Inject // dict value ts value typealias ReadReceiptContent = Map>>> +private const val READ_KEY = "m.read" +private const val TIMESTAMP_KEY = "ts" + internal class ReadReceiptHandler @Inject constructor() { - fun handle(realm: Realm, roomId: String, content: ReadReceiptContent?) { + fun handle(realm: Realm, roomId: String, content: ReadReceiptContent?, isInitialSync: Boolean) { if (content == null) { return } try { - handleReadReceiptContent(realm, roomId, content) + handleReadReceiptContent(realm, roomId, content, isInitialSync) } catch (exception: Exception) { Timber.e("Fail to handle read receipt for room $roomId") } } - private fun handleReadReceiptContent(realm: Realm, roomId: String, content: ReadReceiptContent) { + private fun handleReadReceiptContent(realm: Realm, roomId: String, content: ReadReceiptContent, isInitialSync: Boolean) { + if (isInitialSync) { + initialSyncStrategy(realm, roomId, content) + } else { + incrementalSyncStrategy(realm, roomId, content) + } + } + + + private fun initialSyncStrategy(realm: Realm, roomId: String, content: ReadReceiptContent) { + val readReceiptSummaries = ArrayList() for ((eventId, receiptDict) in content) { - val userIdsDict = receiptDict["m.read"] ?: continue + val userIdsDict = receiptDict[READ_KEY] ?: continue + val readReceiptsSummary = ReadReceiptsSummaryEntity(eventId = eventId) + + for ((userId, paramsDict) in userIdsDict) { + val ts = paramsDict[TIMESTAMP_KEY] ?: 0.0 + val primaryKey = "${roomId}_$userId" + val receiptEntity = ReadReceiptEntity().apply { + this.primaryKey = primaryKey + this.eventId = eventId + this.roomId = roomId + this.userId = userId + this.originServerTs = ts + } + readReceiptsSummary.readReceipts.add(receiptEntity) + } + readReceiptSummaries.add(readReceiptsSummary) + } + realm.insertOrUpdate(readReceiptSummaries) + } + + private fun incrementalSyncStrategy(realm: Realm, roomId: String, content: ReadReceiptContent) { + for ((eventId, receiptDict) in content) { + val userIdsDict = receiptDict[READ_KEY] ?: 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 ts = paramsDict[TIMESTAMP_KEY] ?: 0.0 val primaryKey = "${roomId}_$userId" val receiptEntity = ReadReceiptEntity.where(realm, roomId, userId).findFirst() ?: realm.createObject(ReadReceiptEntity::class.java, primaryKey) @@ -69,4 +104,5 @@ internal class ReadReceiptHandler @Inject constructor() { } } } + } \ 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 a16cae1861..4a243adf42 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 @@ -62,11 +62,11 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch data class LEFT(val data: Map) : HandlingStrategy() } - fun handle(roomsSyncResponse: RoomsSyncResponse, reporter: DefaultInitialSyncProgressService? = null) { + fun handle(roomsSyncResponse: RoomsSyncResponse, isInitialSync: Boolean, reporter: DefaultInitialSyncProgressService? = null) { monarchy.runTransactionSync { realm -> - handleRoomSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), reporter) - handleRoomSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), reporter) - handleRoomSync(realm, HandlingStrategy.LEFT(roomsSyncResponse.leave), reporter) + handleRoomSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), isInitialSync, reporter) + handleRoomSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), isInitialSync, reporter) + handleRoomSync(realm, HandlingStrategy.LEFT(roomsSyncResponse.leave), isInitialSync, reporter) } //handle event for bing rule checks @@ -89,12 +89,12 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch // PRIVATE METHODS ***************************************************************************** - private fun handleRoomSync(realm: Realm, handlingStrategy: HandlingStrategy, reporter: DefaultInitialSyncProgressService?) { + private fun handleRoomSync(realm: Realm, handlingStrategy: HandlingStrategy, isInitialSync: Boolean, reporter: DefaultInitialSyncProgressService?) { val rooms = when (handlingStrategy) { is HandlingStrategy.JOINED -> handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_joined_rooms, 0.6f) { - handleJoinedRoom(realm, it.key, it.value) + handleJoinedRoom(realm, it.key, it.value, isInitialSync) } is HandlingStrategy.INVITED -> handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_invited_rooms, 0.4f) { @@ -112,7 +112,8 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch private fun handleJoinedRoom(realm: Realm, roomId: String, - roomSync: RoomSync): RoomEntity { + roomSync: RoomSync, + isInitalSync: Boolean): RoomEntity { Timber.v("Handle join sync for room $roomId") @@ -152,7 +153,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch roomSummaryUpdater.update(realm, roomId, Membership.JOIN, roomSync.summary, roomSync.unreadNotifications) if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) { - handleEphemeral(realm, roomId, roomSync.ephemeral) + handleEphemeral(realm, roomId, roomSync.ephemeral, isInitalSync) } if (roomSync.accountData != null && roomSync.accountData.events.isNullOrEmpty().not()) { @@ -236,11 +237,12 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch @Suppress("UNCHECKED_CAST") private fun handleEphemeral(realm: Realm, roomId: String, - ephemeral: RoomSyncEphemeral) { + ephemeral: RoomSyncEphemeral, + isInitalSync: Boolean) { for (event in ephemeral.events) { if (event.type != EventType.RECEIPT) continue val readReceiptContent = event.content as? ReadReceiptContent ?: continue - readReceiptHandler.handle(realm, roomId, readReceiptContent) + readReceiptHandler.handle(realm, roomId, readReceiptContent, isInitalSync) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt index fafa758c2d..991e5a9a1a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt @@ -66,7 +66,7 @@ internal class SyncResponseHandler @Inject constructor(private val roomSyncHandl reportSubtask(reporter, R.string.initial_sync_start_importing_account_rooms, 100, 0.7f) { if (syncResponse.rooms != null) { - roomSyncHandler.handle(syncResponse.rooms, reporter) + roomSyncHandler.handle(syncResponse.rooms, isInitialSync, reporter) } } }.also { From 39f58d048b61576ce336c359bbd11a416090bc01 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 8 Aug 2019 17:49:31 +0200 Subject: [PATCH 03/14] Read receipts: fix dummy being overrided --- .../database/helper/ChunkEntityHelper.kt | 28 +++++++++------ .../query/ReadReceiptEntityQueries.kt | 20 ++++++++++- .../query/ReadReceiptsSummaryEntityQueries.kt | 2 +- .../session/sync/ReadReceiptHandler.kt | 34 +++++++------------ .../internal/session/sync/RoomSyncHandler.kt | 16 ++++----- 5 files changed, 58 insertions(+), 42 deletions(-) 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 5a76741ede..a541e8cf39 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 @@ -28,6 +28,7 @@ import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntit 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 +import im.vector.matrix.android.internal.database.query.getOrCreate import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.extensions.assertIsManaged import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection @@ -138,20 +139,25 @@ internal fun ChunkEntity.add(roomId: String, val eventId = event.eventId ?: "" val senderId = event.senderId ?: "" - val currentReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst() - ?: ReadReceiptsSummaryEntity(eventId) + val readReceiptsSummaryEntity = 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) + // Update RR for the sender of a new message with a dummy one + + if (event.originServerTs != null) { + val timestampOfEvent = event.originServerTs.toDouble() + val readReceiptOfSender = ReadReceiptEntity.getOrCreate(realm, roomId = roomId, userId = senderId) + // If the synced RR is older, update + if (timestampOfEvent > readReceiptOfSender.originServerTs) { + val previousReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId = readReceiptOfSender.eventId).findFirst() + readReceiptOfSender.eventId = eventId + readReceiptOfSender.originServerTs = timestampOfEvent + previousReceiptsSummary?.readReceipts?.remove(readReceiptOfSender) + readReceiptsSummaryEntity.readReceipts.add(readReceiptOfSender) } } + val eventEntity = TimelineEventEntity(localId).also { it.root = event.toEntity(roomId).apply { this.stateIndex = currentStateIndex @@ -162,7 +168,7 @@ internal fun ChunkEntity.add(roomId: String, it.eventId = eventId it.roomId = roomId it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() - it.readReceipts = currentReceiptsSummary + it.readReceipts = readReceiptsSummaryEntity } val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size timelineEvents.add(position, eventEntity) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadReceiptEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadReceiptEntityQueries.kt index e5a1afb602..acac419946 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadReceiptEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadReceiptEntityQueries.kt @@ -26,4 +26,22 @@ internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, use return realm.where() .equalTo(ReadReceiptEntityFields.ROOM_ID, roomId) .equalTo(ReadReceiptEntityFields.USER_ID, userId) -} \ No newline at end of file +} + +internal fun ReadReceiptEntity.Companion.createUnmanaged(roomId: String, eventId: String, userId: String, originServerTs: Double): ReadReceiptEntity { + return ReadReceiptEntity().apply { + this.primaryKey = "${roomId}_$userId" + this.eventId = eventId + this.roomId = roomId + this.userId = userId + this.originServerTs = originServerTs + } +} + +internal fun ReadReceiptEntity.Companion.getOrCreate(realm: Realm, roomId: String, userId: String): ReadReceiptEntity { + return ReadReceiptEntity.where(realm, roomId, userId).findFirst() + ?: realm.createObject(ReadReceiptEntity::class.java, "${roomId}_$userId").apply { + this.roomId = roomId + this.userId = userId + } +} 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 index e6c1e68552..d04ced119c 100644 --- 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 @@ -25,4 +25,4 @@ 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/sync/ReadReceiptHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/ReadReceiptHandler.kt index 35acb527d5..5098e824f9 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 @@ -18,6 +18,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.createUnmanaged +import im.vector.matrix.android.internal.database.query.getOrCreate import im.vector.matrix.android.internal.database.query.where import io.realm.Realm import timber.log.Timber @@ -64,14 +66,7 @@ internal class ReadReceiptHandler @Inject constructor() { for ((userId, paramsDict) in userIdsDict) { val ts = paramsDict[TIMESTAMP_KEY] ?: 0.0 - val primaryKey = "${roomId}_$userId" - val receiptEntity = ReadReceiptEntity().apply { - this.primaryKey = primaryKey - this.eventId = eventId - this.roomId = roomId - this.userId = userId - this.originServerTs = ts - } + val receiptEntity = ReadReceiptEntity.createUnmanaged(roomId, eventId, userId, ts) readReceiptsSummary.readReceipts.add(receiptEntity) } readReceiptSummaries.add(readReceiptsSummary) @@ -87,22 +82,19 @@ internal class ReadReceiptHandler @Inject constructor() { for ((userId, paramsDict) in userIdsDict) { val ts = paramsDict[TIMESTAMP_KEY] ?: 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) + val receiptEntity = ReadReceiptEntity.getOrCreate(realm, roomId, userId) + // ensure new ts is superior to the previous one + if (ts > receiptEntity.originServerTs) { + ReadReceiptsSummaryEntity.where(realm, receiptEntity.eventId).findFirst()?.also { + it.readReceipts.remove(receiptEntity) + } + receiptEntity.eventId = eventId + receiptEntity.originServerTs = ts + readReceiptsSummary.readReceipts.add(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 4a243adf42..74b56e774c 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,6 +117,14 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch Timber.v("Handle join sync for room $roomId") + if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) { + handleEphemeral(realm, roomId, roomSync.ephemeral, isInitalSync) + } + + if (roomSync.accountData != null && roomSync.accountData.events.isNullOrEmpty().not()) { + handleRoomAccountDataEvents(realm, roomId, roomSync.accountData) + } + val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) @@ -151,14 +159,6 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch roomEntity.addOrUpdate(chunkEntity) } roomSummaryUpdater.update(realm, roomId, Membership.JOIN, roomSync.summary, roomSync.unreadNotifications) - - if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) { - handleEphemeral(realm, roomId, roomSync.ephemeral, isInitalSync) - } - - if (roomSync.accountData != null && roomSync.accountData.events.isNullOrEmpty().not()) { - handleRoomAccountDataEvents(realm, roomId, roomSync.accountData) - } return roomEntity } From c313ce78cb58d8f9a56550396873ead9ef46bb06 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 8 Aug 2019 17:49:50 +0200 Subject: [PATCH 04/14] Read receipts: sort descending by timestamp --- .../mapper/ReadReceiptsSummaryMapper.kt | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) 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 index 3887f3aca9..b7cdabfc4d 100644 --- 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 @@ -17,6 +17,7 @@ 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.ReadReceiptEntityFields 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 @@ -25,15 +26,20 @@ import io.realm.Realm import io.realm.RealmConfiguration import javax.inject.Inject -internal class ReadReceiptsSummaryMapper @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration){ +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()) - } + val readReceipts = readReceiptsSummaryEntity.readReceipts + readReceipts + .mapNotNull { + val user = UserEntity.where(realm, it.userId).findFirst() + ?: return@mapNotNull null + ReadReceipt(user.asDomain(), it.originServerTs.toLong()) + } + .sortedByDescending { + it.originServerTs + } } } From 825463d9cdae36bda1018da253fab7e01f2ce5cf Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 8 Aug 2019 17:50:16 +0200 Subject: [PATCH 05/14] Change package for NotificationAreaView --- .../core/{platform => ui/views}/NotificationAreaView.kt | 5 +---- .../riotx/features/home/room/detail/RoomDetailFragment.kt | 2 +- vector/src/main/res/layout/fragment_room_detail.xml | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) rename vector/src/main/java/im/vector/riotx/core/{platform => ui/views}/NotificationAreaView.kt (97%) diff --git a/vector/src/main/java/im/vector/riotx/core/platform/NotificationAreaView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/NotificationAreaView.kt similarity index 97% rename from vector/src/main/java/im/vector/riotx/core/platform/NotificationAreaView.kt rename to vector/src/main/java/im/vector/riotx/core/ui/views/NotificationAreaView.kt index a321fd1a20..b159b7b03b 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/NotificationAreaView.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/NotificationAreaView.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.riotx.core.platform +package im.vector.riotx.core.ui.views import android.content.Context import android.graphics.Color @@ -32,10 +32,7 @@ import androidx.core.content.ContextCompat import butterknife.BindView import butterknife.ButterKnife import im.vector.matrix.android.api.failure.MatrixError -import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.events.model.toModel -import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent import im.vector.riotx.R import im.vector.riotx.core.error.ResourceLimitErrorFormatter import im.vector.riotx.features.themes.ThemeUtils diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 4d691f50e7..fad93d38af 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -76,7 +76,7 @@ import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.extensions.setTextOrHide import im.vector.riotx.core.files.addEntryToDownloadManager import im.vector.riotx.core.glide.GlideApp -import im.vector.riotx.core.platform.NotificationAreaView +import im.vector.riotx.core.ui.views.NotificationAreaView import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.utils.* import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter diff --git a/vector/src/main/res/layout/fragment_room_detail.xml b/vector/src/main/res/layout/fragment_room_detail.xml index 81a5b33dad..d84c4637f2 100644 --- a/vector/src/main/res/layout/fragment_room_detail.xml +++ b/vector/src/main/res/layout/fragment_room_detail.xml @@ -98,7 +98,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> - Date: Thu, 8 Aug 2019 17:51:06 +0200 Subject: [PATCH 06/14] Read receipts: create custom view to use it wherever we want easily --- .../riotx/core/ui/views/ReadReceiptsView.kt | 77 +++++++++++++++++++ .../timeline/factory/NoticeItemFactory.kt | 13 +--- .../detail/timeline/item/AbsMessageItem.kt | 38 +-------- .../room/detail/timeline/item/NoticeItem.kt | 3 + .../res/layout/item_timeline_event_base.xml | 59 +------------- .../item_timeline_event_base_noinfo.xml | 9 +++ .../main/res/layout/view_read_receipts.xml | 57 ++++++++++++-- vector/src/main/res/values/styles_riot.xml | 1 + 8 files changed, 153 insertions(+), 104 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt new file mode 100644 index 0000000000..12d5861477 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt @@ -0,0 +1,77 @@ +/* + * 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.riotx.core.ui.views + +import android.content.Context +import android.util.AttributeSet +import android.widget.ImageView +import android.widget.LinearLayout +import androidx.core.view.isVisible +import butterknife.ButterKnife +import im.vector.riotx.R +import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData +import kotlinx.android.synthetic.main.view_read_receipts.view.* + +private const val MAX_RECEIPT_DISPLAYED = 5 + +class ReadReceiptsView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : LinearLayout(context, attrs, defStyleAttr) { + + private val receiptAvatars: List by lazy { + listOf(receiptAvatar1, receiptAvatar2, receiptAvatar3, receiptAvatar4, receiptAvatar5) + } + + init { + setupView() + } + + private fun setupView() { + inflate(context, R.layout.view_read_receipts, this) + ButterKnife.bind(this) + } + + fun render(readReceipts: List, avatarRenderer: AvatarRenderer) { + if (readReceipts.isNotEmpty()) { + isVisible = true + for (index in 0 until MAX_RECEIPT_DISPLAYED) { + val receiptData = readReceipts.getOrNull(index) + if (receiptData == null) { + receiptAvatars[index].isVisible = false + } else { + receiptAvatars[index].isVisible = true + avatarRenderer.render(receiptData.avatarUrl, receiptData.userId, receiptData.displayName, receiptAvatars[index]) + } + } + if (readReceipts.size > MAX_RECEIPT_DISPLAYED) { + receiptMore.isVisible = true + receiptMore.text = context.getString( + R.string.x_plus, readReceipts.size - MAX_RECEIPT_DISPLAYED + ) + } else { + receiptMore.isVisible = false + } + } else { + isVisible = false + } + + } + +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt index 52771ad6e9..b1cb540786 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt @@ -25,23 +25,18 @@ import im.vector.riotx.features.home.room.detail.timeline.helper.senderName import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem_ +import im.vector.riotx.features.home.room.detail.timeline.util.MessageInformationDataFactory import javax.inject.Inject class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEventFormatter, - private val avatarRenderer: AvatarRenderer) { + private val avatarRenderer: AvatarRenderer, + private val informationDataFactory: MessageInformationDataFactory) { fun create(event: TimelineEvent, highlight: Boolean, callback: TimelineEventController.Callback?): NoticeItem? { val formattedText = eventFormatter.format(event) ?: return null - val informationData = MessageInformationData( - eventId = event.root.eventId ?: "?", - senderId = event.root.senderId ?: "", - sendState = event.root.sendState, - avatarUrl = event.senderAvatar(), - memberName = event.senderName(), - showInformation = false - ) + val informationData = informationDataFactory.create(event, null) return NoticeItem_() .avatarRenderer(avatarRenderer) 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 1462e842d9..51d2ce92fa 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 @@ -33,6 +33,7 @@ import com.airbnb.epoxy.EpoxyAttribute import im.vector.matrix.android.api.session.room.send.SendState import im.vector.riotx.R import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.core.ui.views.ReadReceiptsView import im.vector.riotx.core.utils.DebouncedClickListener import im.vector.riotx.core.utils.DimensionUtils.dpToPx import im.vector.riotx.features.home.AvatarRenderer @@ -40,8 +41,6 @@ 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() { @EpoxyAttribute @@ -125,28 +124,7 @@ 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 - } + holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer) if (!shouldShowReactionAtBottom() || informationData.orderedReactionList.isNullOrEmpty()) { holder.reactionWrapper?.isVisible = false @@ -198,17 +176,7 @@ 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) - } - + val readReceiptsView by bind(R.id.readReceiptsView) var reactionWrapper: ViewGroup? = null var reactionFlowHelper: Flow? = null } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt index 2879073f18..b18a665d6b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt @@ -22,6 +22,7 @@ import android.widget.TextView import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R +import im.vector.riotx.core.ui.views.ReadReceiptsView import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController @@ -55,6 +56,7 @@ abstract class NoticeItem : BaseEventItem() { holder.avatarImageView ) holder.view.setOnLongClickListener(longClickListener) + holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer) } override fun getViewType() = STUB_ID @@ -62,6 +64,7 @@ abstract class NoticeItem : BaseEventItem() { class Holder : BaseHolder(STUB_ID) { val avatarImageView by bind(R.id.itemNoticeAvatarView) val noticeTextView by bind(R.id.itemNoticeTextView) + val readReceiptsView by bind(R.id.readReceiptsView) } companion object { 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 a8b81606e1..2f0be78f38 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -123,65 +123,14 @@ - - - - - - - - - - - - - - - + app:layout_constraintEnd_toEndOf="parent" /> \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml index 004428eccf..7726839902 100644 --- a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml +++ b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml @@ -52,5 +52,14 @@ android:layout="@layout/item_timeline_event_merged_header_stub" tools:ignore="MissingConstraints" /> + + \ 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 index 4f65a82a60..e3cbc6ba06 100644 --- a/vector/src/main/res/layout/view_read_receipts.xml +++ b/vector/src/main/res/layout/view_read_receipts.xml @@ -1,11 +1,58 @@ - + android:layout_width="wrap_content" + android:layout_height="wrap_content" + tools:parentTag="android.widget.LinearLayout"> + + - + + + + + + + + + diff --git a/vector/src/main/res/values/styles_riot.xml b/vector/src/main/res/values/styles_riot.xml index eaf0530b96..80f5148a6e 100644 --- a/vector/src/main/res/values/styles_riot.xml +++ b/vector/src/main/res/values/styles_riot.xml @@ -297,6 +297,7 @@ From 70639f180cd0fe5276c33d7a44b1fe21d7e47877 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 8 Aug 2019 19:59:20 +0200 Subject: [PATCH 07/14] Read receipts: add read receipts bottom sheet --- .../main/java/im/vector/matrix/rx/RxRoom.kt | 5 + .../api/session/room/read/ReadService.kt | 4 + .../internal/session/room/RoomFactory.kt | 4 +- .../session/room/read/DefaultReadService.kt | 28 +++++- .../date/VectorDateFormatter.kt} | 14 ++- .../vector/riotx/core/di/ScreenComponent.kt | 9 +- .../vector/riotx/core/di/ViewModelModule.kt | 23 ++++- .../riotx/core/ui/views/ReadReceiptsView.kt | 4 +- .../home/room/detail/RoomDetailFragment.kt | 54 ++++++----- .../readreceipts/DisplayReadReceiptItem.kt | 55 +++++++++++ .../DisplayReadReceiptsBottomSheet.kt | 93 +++++++++++++++++++ .../DisplayReadReceiptsController.kt | 71 ++++++++++++++ .../DisplayReadReceiptsViewModel.kt | 63 +++++++++++++ .../DisplayReadReceiptsViewState.kt | 33 +++++++ .../timeline/TimelineEventController.kt | 18 ++-- .../action/ViewEditHistoryBottomSheet.kt | 2 +- .../action/ViewEditHistoryEpoxyController.kt | 8 +- .../action/ViewEditHistoryViewModel.kt | 4 +- .../timeline/action/ViewReactionViewModel.kt | 20 ++-- .../action/ViewReactionsEpoxyController.kt | 1 + .../timeline/factory/MessageItemFactory.kt | 12 ++- .../timeline/factory/NoticeItemFactory.kt | 1 + .../detail/timeline/item/AbsMessageItem.kt | 9 +- .../timeline/item/MessageInformationData.kt | 1 - .../room/detail/timeline/item/NoticeItem.kt | 10 +- .../util/MessageInformationDataFactory.kt | 8 +- .../home/room/list/RoomSummaryItemFactory.kt | 11 +-- .../res/layout/item_display_read_receipt.xml | 43 +++++++++ 28 files changed, 535 insertions(+), 73 deletions(-) rename vector/src/main/java/im/vector/riotx/{features/home/room/detail/timeline/helper/TimelineDateFormatter.kt => core/date/VectorDateFormatter.kt} (69%) create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptItem.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsController.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewModel.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewState.kt create mode 100644 vector/src/main/res/layout/item_display_read_receipt.xml diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt index 201622b32f..0ff0987dfe 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt @@ -18,6 +18,7 @@ package im.vector.matrix.rx import im.vector.matrix.android.api.session.room.Room 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.RoomSummary import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import io.reactivex.Observable @@ -49,6 +50,10 @@ class RxRoom(private val room: Room) { room.join(viaServers, MatrixCallbackSingle(it)).toSingle(it) } + fun liveEventReadReceipts(eventId: String): Observable> { + return room.getEventReadReceiptsLive(eventId).asObservable() + } + } fun Room.rx(): RxRoom { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt index ab406b8e54..d97fc497f0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt @@ -16,7 +16,9 @@ package im.vector.matrix.android.api.session.room.read +import androidx.lifecycle.LiveData import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.room.model.ReadReceipt /** * This interface defines methods to handle read receipts and read marker in a room. It's implemented at the room level. @@ -39,4 +41,6 @@ interface ReadService { fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback) fun isEventRead(eventId: String): Boolean + + fun getEventReadReceiptsLive(eventId: String): LiveData> } \ 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 dd5d2d3b97..cf2627b0b4 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 @@ -21,6 +21,7 @@ import com.zhuinden.monarchy.Monarchy 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.ReadReceiptsSummaryMapper 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 @@ -49,6 +50,7 @@ internal class RoomFactory @Inject constructor(private val context: Context, private val eventFactory: LocalEchoEventFactory, private val roomSummaryMapper: RoomSummaryMapper, private val timelineEventMapper: TimelineEventMapper, + private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, private val taskExecutor: TaskExecutor, private val loadRoomMembersTask: LoadRoomMembersTask, private val inviteTask: InviteTask, @@ -67,7 +69,7 @@ internal class RoomFactory @Inject constructor(private val context: Context, 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 readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask, readReceiptsSummaryMapper, credentials) val relationService = DefaultRelationService(context, credentials, roomId, eventFactory, cryptoService, findReactionEventForUndoTask, fetchEditHistoryTask, monarchy, taskExecutor) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt index 2e30c12ef6..3df872bf4b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt @@ -16,12 +16,21 @@ package im.vector.matrix.android.internal.session.room.read +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.data.Credentials +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.read.ReadService +import im.vector.matrix.android.internal.database.RealmLiveData +import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper +import im.vector.matrix.android.internal.database.mapper.asDomain 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.query.find import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom import im.vector.matrix.android.internal.database.query.where @@ -33,6 +42,7 @@ internal class DefaultReadService @Inject constructor(private val roomId: String private val monarchy: Monarchy, private val taskExecutor: TaskExecutor, private val setReadMarkersTask: SetReadMarkersTask, + private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, private val credentials: Credentials) : ReadService { override fun markAllAsRead(callback: MatrixCallback) { @@ -67,16 +77,26 @@ internal class DefaultReadService @Inject constructor(private val roomId: String var isEventRead = false monarchy.doWithRealm { val readReceipt = ReadReceiptEntity.where(it, roomId, credentials.userId).findFirst() - ?: return@doWithRealm + ?: return@doWithRealm val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(it, roomId) - ?: return@doWithRealm + ?: return@doWithRealm val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex - ?: Int.MIN_VALUE + ?: Int.MIN_VALUE val eventToCheckIndex = liveChunk.timelineEvents.find(eventId)?.root?.displayIndex - ?: Int.MAX_VALUE + ?: Int.MAX_VALUE isEventRead = eventToCheckIndex <= readReceiptIndex } return isEventRead } + override fun getEventReadReceiptsLive(eventId: String): LiveData> { + val liveEntity = RealmLiveData(monarchy.realmConfiguration) { realm -> + ReadReceiptsSummaryEntity.where(realm, eventId) + } + return Transformations.map(liveEntity) { realmResults -> + realmResults.firstOrNull()?.let { + readReceiptsSummaryMapper.map(it) + } ?: emptyList() + } + } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDateFormatter.kt b/vector/src/main/java/im/vector/riotx/core/date/VectorDateFormatter.kt similarity index 69% rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDateFormatter.kt rename to vector/src/main/java/im/vector/riotx/core/date/VectorDateFormatter.kt index a1104e3285..540400d570 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDateFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/core/date/VectorDateFormatter.kt @@ -14,15 +14,18 @@ * limitations under the License. */ -package im.vector.riotx.features.home.room.detail.timeline.helper +package im.vector.riotx.core.date +import android.content.Context +import android.text.format.DateUtils import im.vector.riotx.core.resources.LocaleProvider import org.threeten.bp.LocalDateTime import org.threeten.bp.format.DateTimeFormatter import javax.inject.Inject -class TimelineDateFormatter @Inject constructor (private val localeProvider: LocaleProvider) { +class VectorDateFormatter @Inject constructor(private val context: Context, + private val localeProvider: LocaleProvider) { private val messageHourFormatter by lazy { DateTimeFormatter.ofPattern("H:mm", localeProvider.current()) @@ -39,4 +42,11 @@ class TimelineDateFormatter @Inject constructor (private val localeProvider: Loc return messageDayFormatter.format(localDateTime) } + fun formatRelativeDateTime(time: Long?): String { + if (time == null) { + return "" + } + return DateUtils.getRelativeDateTimeString(context, time, DateUtils.DAY_IN_MILLIS, 2 * DateUtils.DAY_IN_MILLIS, DateUtils.FORMAT_SHOW_WEEKDAY).toString() + } + } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index 35cda2e6c6..d5445862ba 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt @@ -41,7 +41,12 @@ import im.vector.riotx.features.home.createdirect.CreateDirectRoomDirectoryUsers import im.vector.riotx.features.home.createdirect.CreateDirectRoomKnownUsersFragment import im.vector.riotx.features.home.group.GroupListFragment import im.vector.riotx.features.home.room.detail.RoomDetailFragment -import im.vector.riotx.features.home.room.detail.timeline.action.* +import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet +import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet +import im.vector.riotx.features.home.room.detail.timeline.action.MessageMenuFragment +import im.vector.riotx.features.home.room.detail.timeline.action.QuickReactionFragment +import im.vector.riotx.features.home.room.detail.timeline.action.ViewEditHistoryBottomSheet +import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionBottomSheet import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity import im.vector.riotx.features.home.room.list.RoomListFragment import im.vector.riotx.features.invite.VectorInviteView @@ -165,6 +170,8 @@ interface ScreenComponent { fun inject(createDirectRoomActivity: CreateDirectRoomActivity) + fun inject(displayReadReceiptsBottomSheet: DisplayReadReceiptsBottomSheet) + @Component.Factory interface Factory { fun create(vectorComponent: VectorComponent, diff --git a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt index 80410f879f..c2c86cad46 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt @@ -29,7 +29,11 @@ import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsVie import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsViewModel_AssistedFactory import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupSharedViewModel import im.vector.riotx.features.crypto.verification.SasVerificationViewModel -import im.vector.riotx.features.home.* +import im.vector.riotx.features.home.HomeActivityViewModel +import im.vector.riotx.features.home.HomeActivityViewModel_AssistedFactory +import im.vector.riotx.features.home.HomeDetailViewModel +import im.vector.riotx.features.home.HomeDetailViewModel_AssistedFactory +import im.vector.riotx.features.home.HomeNavigationViewModel import im.vector.riotx.features.home.createdirect.CreateDirectRoomNavigationViewModel import im.vector.riotx.features.home.createdirect.CreateDirectRoomViewModel import im.vector.riotx.features.home.createdirect.CreateDirectRoomViewModel_AssistedFactory @@ -39,7 +43,18 @@ import im.vector.riotx.features.home.room.detail.RoomDetailViewModel import im.vector.riotx.features.home.room.detail.RoomDetailViewModel_AssistedFactory import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel_AssistedFactory -import im.vector.riotx.features.home.room.detail.timeline.action.* +import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsViewModel +import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsViewModel_AssistedFactory +import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsViewModel +import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsViewModel_AssistedFactory +import im.vector.riotx.features.home.room.detail.timeline.action.MessageMenuViewModel +import im.vector.riotx.features.home.room.detail.timeline.action.MessageMenuViewModel_AssistedFactory +import im.vector.riotx.features.home.room.detail.timeline.action.QuickReactionViewModel +import im.vector.riotx.features.home.room.detail.timeline.action.QuickReactionViewModel_AssistedFactory +import im.vector.riotx.features.home.room.detail.timeline.action.ViewEditHistoryViewModel +import im.vector.riotx.features.home.room.detail.timeline.action.ViewEditHistoryViewModel_AssistedFactory +import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionViewModel +import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionViewModel_AssistedFactory import im.vector.riotx.features.home.room.list.RoomListViewModel import im.vector.riotx.features.home.room.list.RoomListViewModel_AssistedFactory import im.vector.riotx.features.reactions.EmojiChooserViewModel @@ -182,4 +197,8 @@ interface ViewModelModule { @Binds fun bindPushGatewaysViewModelFactory(factory: PushGatewaysViewModel_AssistedFactory): PushGatewaysViewModel.Factory + + @Binds + fun bindDisplayReadReceiptsViewModel(factory: DisplayReadReceiptsViewModel_AssistedFactory): DisplayReadReceiptsViewModel.Factory + } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt index 12d5861477..6293e22bd7 100644 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt @@ -23,6 +23,7 @@ import android.widget.LinearLayout import androidx.core.view.isVisible import butterknife.ButterKnife import im.vector.riotx.R +import im.vector.riotx.core.utils.DebouncedClickListener import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData import kotlinx.android.synthetic.main.view_read_receipts.view.* @@ -48,7 +49,8 @@ class ReadReceiptsView @JvmOverloads constructor( ButterKnife.bind(this) } - fun render(readReceipts: List, avatarRenderer: AvatarRenderer) { + fun render(readReceipts: List, avatarRenderer: AvatarRenderer, clickListener: OnClickListener) { + setOnClickListener(clickListener) if (readReceipts.isNotEmpty()) { isVisible = true for (index in 0 until MAX_RECEIPT_DISPLAYED) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index fad93d38af..544f05cd99 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -91,6 +91,7 @@ import im.vector.riotx.features.home.room.detail.composer.TextComposerActions import im.vector.riotx.features.home.room.detail.composer.TextComposerView import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel import im.vector.riotx.features.home.room.detail.composer.TextComposerViewState +import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.action.* import im.vector.riotx.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener @@ -315,17 +316,17 @@ class RoomDetailFragment : if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { val parser = Parser.builder().build() val document = parser.parse(messageContent.formattedBody - ?: messageContent.body) + ?: messageContent.body) formattedBody = eventHtmlRenderer.render(document) } composerLayout.composerRelatedMessageContent.text = formattedBody - ?: nonFormattedBody + ?: nonFormattedBody composerLayout.composerEditText.setText(if (useText) event.getTextEditableContent() else "") composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes)) avatarRenderer.render(event.senderAvatar, event.root.senderId - ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar) + ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar) composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length) composerLayout.expand { @@ -354,9 +355,9 @@ class RoomDetailFragment : REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data) REACTION_SELECT_REQUEST_CODE -> { val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID) - ?: return + ?: return val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT) - ?: return + ?: return //TODO check if already reacted with that? roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId)) } @@ -391,26 +392,26 @@ class RoomDetailFragment : if (VectorPreferences.swipeToReplyIsEnabled(requireContext())) { val swipeCallback = RoomMessageTouchHelperCallback(requireContext(), - R.drawable.ic_reply, - object : RoomMessageTouchHelperCallback.QuickReplayHandler { - override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { - (model as? AbsMessageItem)?.informationData?.let { - val eventId = it.eventId - roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId)) - } - } + R.drawable.ic_reply, + object : RoomMessageTouchHelperCallback.QuickReplayHandler { + override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { + (model as? AbsMessageItem)?.informationData?.let { + val eventId = it.eventId + roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId)) + } + } - override fun canSwipeModel(model: EpoxyModel<*>): Boolean { - return when (model) { - is MessageFileItem, - is MessageImageVideoItem, - is MessageTextItem -> { - return (model as AbsMessageItem).informationData.sendState == SendState.SYNCED - } - else -> false - } - } - }) + override fun canSwipeModel(model: EpoxyModel<*>): Boolean { + return when (model) { + is MessageFileItem, + is MessageImageVideoItem, + is MessageTextItem -> { + return (model as AbsMessageItem).informationData.sendState == SendState.SYNCED + } + else -> false + } + } + }) val touchHelper = ItemTouchHelper(swipeCallback) touchHelper.attachToRecyclerView(recyclerView) } @@ -816,6 +817,11 @@ class RoomDetailFragment : }) } + override fun onReadReceiptsClicked(informationData: MessageInformationData) { + DisplayReadReceiptsBottomSheet.newInstance(roomDetailArgs.roomId, informationData) + .show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS") + } + // AutocompleteUserPresenter.Callback override fun onQueryUsers(query: CharSequence?) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptItem.kt new file mode 100644 index 0000000000..fd9b5f111b --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptItem.kt @@ -0,0 +1,55 @@ +/* + * 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.riotx.features.home.room.detail.readreceipts + +import android.widget.ImageView +import android.widget.TextView +import androidx.core.view.isVisible +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.airbnb.epoxy.EpoxyModelWithHolder +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.features.home.AvatarRenderer + +@EpoxyModelClass(layout = R.layout.item_display_read_receipt) +abstract class DisplayReadReceiptItem : EpoxyModelWithHolder() { + + @EpoxyAttribute var name: String? = null + @EpoxyAttribute var userId: String = "" + @EpoxyAttribute var avatarUrl: String? = null + @EpoxyAttribute var timestamp: CharSequence? = null + @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer + + override fun bind(holder: Holder) { + avatarRenderer.render(avatarUrl, userId, name, holder.avatarView) + holder.displayNameView.text = name ?: userId + timestamp?.let { + holder.timestampView.text = it + holder.timestampView.isVisible = true + } ?: run { + holder.timestampView.isVisible = false + } + } + + class Holder : VectorEpoxyHolder() { + val avatarView by bind(R.id.readReceiptAvatar) + val displayNameView by bind(R.id.readReceiptName) + val timestampView by bind(R.id.readReceiptDate) + } + +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt new file mode 100644 index 0000000000..572954f106 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt @@ -0,0 +1,93 @@ +/* + * 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.riotx.features.home.room.detail.readreceipts + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import androidx.recyclerview.widget.DividerItemDecoration +import butterknife.BindView +import butterknife.ButterKnife +import com.airbnb.epoxy.EpoxyRecyclerView +import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.riotx.EmojiCompatFontProvider +import im.vector.riotx.R +import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs +import im.vector.riotx.features.home.room.detail.timeline.action.VectorBaseBottomSheetDialogFragment +import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionBottomSheet +import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData +import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.* +import javax.inject.Inject + +/** + * Bottom sheet displaying list of read receipts for a given event ordered by descending timestamp + */ +class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() { + + private val viewModel: DisplayReadReceiptsViewModel by fragmentViewModel() + + @Inject lateinit var displayReadReceiptsViewModelFactory: DisplayReadReceiptsViewModel.Factory + @Inject lateinit var epoxyController: DisplayReadReceiptsController + + @BindView(R.id.bottom_sheet_display_reactions_list) + lateinit var epoxyRecyclerView: EpoxyRecyclerView + + + override fun injectWith(screenComponent: ScreenComponent) { + screenComponent.inject(this) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val view = inflater.inflate(R.layout.bottom_sheet_epoxylist_with_title, container, false) + ButterKnife.bind(this, view) + return view + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + epoxyRecyclerView.setController(epoxyController) + val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context, + LinearLayout.VERTICAL) + epoxyRecyclerView.addItemDecoration(dividerItemDecoration) + bottomSheetTitle.text = getString(R.string.read_receipts_list) + } + + + override fun invalidate() = withState(viewModel) { + epoxyController.setData(it) + } + + companion object { + fun newInstance(roomId: String, informationData: MessageInformationData): DisplayReadReceiptsBottomSheet { + val args = Bundle() + val parcelableArgs = TimelineEventFragmentArgs( + informationData.eventId, + roomId, + informationData + ) + args.putParcelable(MvRx.KEY_ARG, parcelableArgs) + return DisplayReadReceiptsBottomSheet().apply { arguments = args } + + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsController.kt new file mode 100644 index 0000000000..2c2f9f498c --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsController.kt @@ -0,0 +1,71 @@ +/* + * 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.riotx.features.home.room.detail.readreceipts + +import com.airbnb.epoxy.TypedEpoxyController +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Incomplete +import com.airbnb.mvrx.Success +import im.vector.matrix.android.api.session.Session +import im.vector.riotx.R +import im.vector.riotx.core.date.VectorDateFormatter +import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.core.ui.list.genericFooterItem +import im.vector.riotx.core.ui.list.genericLoaderItem +import im.vector.riotx.features.home.AvatarRenderer +import javax.inject.Inject + +/** + * Epoxy controller for read receipt event list + */ +class DisplayReadReceiptsController @Inject constructor(private val dateFormatter: VectorDateFormatter, + private val stringProvider: StringProvider, + private val session: Session, + private val avatarRender: AvatarRenderer) + : TypedEpoxyController() { + + + override fun buildModels(state: DisplayReadReceiptsViewState) { + when (state.readReceipts) { + is Incomplete -> { + genericLoaderItem { + id("loading") + } + } + is Fail -> { + genericFooterItem { + id("failure") + text(stringProvider.getString(R.string.unknown_error)) + } + } + is Success -> { + state.readReceipts()?.forEach { + val timestamp = dateFormatter.formatRelativeDateTime(it.originServerTs) + DisplayReadReceiptItem_() + .id(it.user.userId) + .userId(it.user.userId) + .avatarUrl(it.user.avatarUrl) + .name(it.user.displayName) + .avatarRenderer(avatarRender) + .timestamp(timestamp) + .addIf(session.myUserId != it.user.userId, this) + } + } + } + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewModel.kt new file mode 100644 index 0000000000..8423ba4ac6 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewModel.kt @@ -0,0 +1,63 @@ +/* + * 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.riotx.features.home.room.detail.readreceipts + +import com.airbnb.mvrx.* +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.rx.RxRoom +import im.vector.riotx.core.platform.VectorViewModel + +/** + * Used to display the list of read receipts to a given event + */ +class DisplayReadReceiptsViewModel @AssistedInject constructor(@Assisted initialState: DisplayReadReceiptsViewState, + private val session: Session +) : VectorViewModel(initialState) { + + private val roomId = initialState.roomId + private val eventId = initialState.eventId + private val room = session.getRoom(roomId) + ?: throw IllegalStateException("Shouldn't use this ViewModel without a room") + + @AssistedInject.Factory + interface Factory { + fun create(initialState: DisplayReadReceiptsViewState): DisplayReadReceiptsViewModel + } + + companion object : MvRxViewModelFactory { + + override fun create(viewModelContext: ViewModelContext, state: DisplayReadReceiptsViewState): DisplayReadReceiptsViewModel? { + val fragment: DisplayReadReceiptsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.displayReadReceiptsViewModelFactory.create(state) + } + } + + init { + observeEventAnnotationSummaries() + } + + private fun observeEventAnnotationSummaries() { + RxRoom(room) + .liveEventReadReceipts(eventId) + .execute { + copy(readReceipts = it) + } + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewState.kt new file mode 100644 index 0000000000..68952b998e --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewState.kt @@ -0,0 +1,33 @@ +/* + * 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.riotx.features.home.room.detail.readreceipts + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized +import im.vector.matrix.android.api.session.room.model.ReadReceipt +import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs + +data class DisplayReadReceiptsViewState( + val eventId: String, + val roomId: String, + val readReceipts: Async> = Uninitialized +) : MvRxState { + + constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId) + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index e895630910..28a3d100d1 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt @@ -27,6 +27,7 @@ import com.airbnb.epoxy.EpoxyModel import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.epoxy.LoadingItem_ import im.vector.riotx.core.extensions.localDateTime import im.vector.riotx.core.resources.UserPreferencesProvider @@ -42,7 +43,7 @@ import im.vector.riotx.features.media.VideoContentRenderer import org.threeten.bp.LocalDateTime import javax.inject.Inject -class TimelineEventController @Inject constructor(private val dateFormatter: TimelineDateFormatter, +class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter, private val timelineItemFactory: TimelineItemFactory, private val timelineMediaSizeProvider: TimelineMediaSizeProvider, private val avatarRenderer: AvatarRenderer, @@ -51,7 +52,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim userPreferencesProvider: UserPreferencesProvider ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener { - interface Callback : ReactionPillCallback, AvatarCallback, BaseCallback, UrlClickCallback { + interface Callback : BaseCallback, ReactionPillCallback, AvatarCallback, UrlClickCallback, ReadReceiptsCallback { fun onEventVisible(event: TimelineEvent) fun onRoomCreateLinkClicked(url: String) fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View) @@ -77,6 +78,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim fun onMemberNameClicked(informationData: MessageInformationData) } + interface ReadReceiptsCallback { + fun onReadReceiptsClicked(informationData: MessageInformationData) + } + interface UrlClickCallback { fun onUrlClicked(url: String): Boolean fun onUrlLongClicked(url: String): Boolean @@ -158,7 +163,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim synchronized(modelCache) { for (i in 0 until modelCache.size) { if (modelCache[i]?.eventId == eventIdToHighlight - || modelCache[i]?.eventId == this.eventIdToHighlight) { + || modelCache[i]?.eventId == this.eventIdToHighlight) { modelCache[i] = null } } @@ -219,8 +224,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim // Should be build if not cached or if cached but contains mergedHeader or formattedDay // We then are sure we always have items up to date. if (modelCache[position] == null - || modelCache[position]?.mergedHeaderModel != null - || modelCache[position]?.formattedDayModel != null) { + || modelCache[position]?.mergedHeaderModel != null + || modelCache[position]?.formattedDayModel != null) { modelCache[position] = buildItemModels(position, currentSnapshot) } } @@ -293,7 +298,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim // We try to find if one of the item id were used as mergeItemCollapseStates key // => handle case where paginating from mergeable events and we get more val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull() - val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey) ?: true + val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey) + ?: true val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { initialCollapseState } if (isCollapsed) { collapsedEventIds.addAll(mergedEventIds) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryBottomSheet.kt index aefbde431a..be3dfc80d1 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryBottomSheet.kt @@ -49,7 +49,7 @@ class ViewEditHistoryBottomSheet : VectorBaseBottomSheetDialogFragment() { lateinit var epoxyRecyclerView: EpoxyRecyclerView private val epoxyController by lazy { - ViewEditHistoryEpoxyController(requireContext(), viewModel.timelineDateFormatter, eventHtmlRenderer) + ViewEditHistoryEpoxyController(requireContext(), viewModel.dateFormatter, eventHtmlRenderer) } override fun injectWith(screenComponent: ScreenComponent) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryEpoxyController.kt index fc11f25561..dab43108c2 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryEpoxyController.kt @@ -33,7 +33,7 @@ import im.vector.riotx.core.ui.list.genericFooterItem import im.vector.riotx.core.ui.list.genericItem import im.vector.riotx.core.ui.list.genericItemHeader import im.vector.riotx.core.ui.list.genericLoaderItem -import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDateFormatter +import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.features.html.EventHtmlRenderer import me.gujun.android.span.span import name.fraser.neil.plaintext.diff_match_patch @@ -44,7 +44,7 @@ import java.util.* * Epoxy controller for reaction event list */ class ViewEditHistoryEpoxyController(private val context: Context, - val timelineDateFormatter: TimelineDateFormatter, + val dateFormatter: VectorDateFormatter, val eventHtmlRenderer: EventHtmlRenderer) : TypedEpoxyController() { override fun buildModels(state: ViewEditHistoryViewState) { @@ -84,7 +84,7 @@ class ViewEditHistoryEpoxyController(private val context: Context, if (lastDate?.get(Calendar.DAY_OF_YEAR) != evDate.get(Calendar.DAY_OF_YEAR)) { //need to display header with day val dateString = if (DateUtils.isToday(evDate.timeInMillis)) context.getString(R.string.today) - else timelineDateFormatter.formatMessageDay(timelineEvent.localDateTime()) + else dateFormatter.formatMessageDay(timelineEvent.localDateTime()) genericItemHeader { id(evDate.hashCode()) text(dateString) @@ -136,7 +136,7 @@ class ViewEditHistoryEpoxyController(private val context: Context, } genericItem { id(timelineEvent.eventId) - title(timelineDateFormatter.formatMessageHour(timelineEvent.localDateTime())) + title(dateFormatter.formatMessageHour(timelineEvent.localDateTime())) description(spannedDiff ?: body) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryViewModel.kt index 6ad172101a..e8b27e1889 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryViewModel.kt @@ -27,7 +27,7 @@ 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.internal.crypto.algorithms.olm.OlmDecryptionResult import im.vector.riotx.core.platform.VectorViewModel -import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDateFormatter +import im.vector.riotx.core.date.VectorDateFormatter import timber.log.Timber import java.util.* @@ -46,7 +46,7 @@ data class ViewEditHistoryViewState( class ViewEditHistoryViewModel @AssistedInject constructor(@Assisted initialState: ViewEditHistoryViewState, val session: Session, - val timelineDateFormatter: TimelineDateFormatter + val dateFormatter: VectorDateFormatter ) : VectorViewModel(initialState) { private val roomId = initialState.roomId diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionViewModel.kt index 6f5a784742..9479c30783 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionViewModel.kt @@ -16,16 +16,20 @@ package im.vector.riotx.features.home.room.detail.timeline.action -import com.airbnb.mvrx.* +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.ViewModelContext import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.room.model.ReactionAggregatedSummary import im.vector.matrix.rx.RxRoom -import im.vector.riotx.core.extensions.localDateTime import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.utils.isSingleEmoji -import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDateFormatter +import im.vector.riotx.core.date.VectorDateFormatter import io.reactivex.Observable import io.reactivex.Single @@ -54,13 +58,13 @@ data class ReactionInfo( class ViewReactionViewModel @AssistedInject constructor(@Assisted initialState: DisplayReactionsViewState, private val session: Session, - private val timelineDateFormatter: TimelineDateFormatter + private val dateFormatter: VectorDateFormatter ) : VectorViewModel(initialState) { private val roomId = initialState.roomId private val eventId = initialState.eventId private val room = session.getRoom(roomId) - ?: throw IllegalStateException("Shouldn't use this ViewModel without a room") + ?: throw IllegalStateException("Shouldn't use this ViewModel without a room") @AssistedInject.Factory interface Factory { @@ -100,14 +104,14 @@ class ViewReactionViewModel @AssistedInject constructor(@Assisted .fromIterable(summary.sourceEvents) .map { val event = room.getTimeLineEvent(it) - ?: throw RuntimeException("Your eventId is not valid") - val localDate = event.root.localDateTime() + ?: throw RuntimeException("Your eventId is not valid") ReactionInfo( event.root.eventId!!, summary.key, event.root.senderId ?: "", event.getDisambiguatedDisplayName(), - timelineDateFormatter.formatMessageHour(localDate) + dateFormatter.formatRelativeDateTime(event.root.originServerTs) + ) } }.toList() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionsEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionsEpoxyController.kt index 74b3f4925f..e3df0b7300 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionsEpoxyController.kt @@ -18,6 +18,7 @@ package im.vector.riotx.features.home.room.detail.timeline.action import android.content.Context import android.graphics.Typeface +import android.text.format.DateUtils import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Incomplete 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 5ed7bcb3af..71e7627d63 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 @@ -161,6 +161,7 @@ class MessageItemFactory @Inject constructor( .informationData(informationData) .highlighted(highlight) .avatarCallback(callback) + .readReceiptsCallback(callback) .filename(messageContent.body) .iconRes(R.drawable.filetype_audio) .reactionPillCallback(callback) @@ -191,6 +192,7 @@ class MessageItemFactory @Inject constructor( .avatarCallback(callback) .filename(messageContent.body) .reactionPillCallback(callback) + .readReceiptsCallback(callback) .emojiTypeFace(emojiCompatFontProvider.typeface) .iconRes(R.drawable.filetype_attachment) .cellClickListener( @@ -205,10 +207,6 @@ class MessageItemFactory @Inject constructor( DebouncedClickListener(View.OnClickListener { _ -> callback?.onFileMessageClicked(informationData.eventId, messageContent) })) - .longClickListener { view -> - return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false - } } private fun buildNotHandledMessageItem(messageContent: MessageContent, highlight: Boolean): DefaultItem? { @@ -246,6 +244,7 @@ class MessageItemFactory @Inject constructor( .avatarCallback(callback) .mediaData(data) .reactionPillCallback(callback) + .readReceiptsCallback(callback) .emojiTypeFace(emojiCompatFontProvider.typeface) .clickListener( DebouncedClickListener(View.OnClickListener { view -> @@ -297,6 +296,7 @@ class MessageItemFactory @Inject constructor( .avatarCallback(callback) .mediaData(thumbnailData) .reactionPillCallback(callback) + .readReceiptsCallback(callback) .emojiTypeFace(emojiCompatFontProvider.typeface) .cellClickListener( DebouncedClickListener(View.OnClickListener { view -> @@ -336,6 +336,7 @@ class MessageItemFactory @Inject constructor( .avatarCallback(callback) .urlClickCallback(callback) .reactionPillCallback(callback) + .readReceiptsCallback(callback) .emojiTypeFace(emojiCompatFontProvider.typeface) //click on the text .cellClickListener( @@ -402,6 +403,7 @@ class MessageItemFactory @Inject constructor( .avatarCallback(callback) .reactionPillCallback(callback) .urlClickCallback(callback) + .readReceiptsCallback(callback) .emojiTypeFace(emojiCompatFontProvider.typeface) .memberClickListener( DebouncedClickListener(View.OnClickListener { view -> @@ -441,6 +443,7 @@ class MessageItemFactory @Inject constructor( .highlighted(highlight) .avatarCallback(callback) .reactionPillCallback(callback) + .readReceiptsCallback(callback) .urlClickCallback(callback) .emojiTypeFace(emojiCompatFontProvider.typeface) .cellClickListener( @@ -462,6 +465,7 @@ class MessageItemFactory @Inject constructor( .informationData(informationData) .highlighted(highlight) .avatarCallback(callback) + .readReceiptsCallback(callback) .cellClickListener( DebouncedClickListener(View.OnClickListener { view -> callback?.onEventCellClicked(informationData, null, view) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt index b1cb540786..f73a200133 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt @@ -44,6 +44,7 @@ class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEv .highlighted(highlight) .informationData(informationData) .baseCallback(callback) + .readReceiptsCallback(callback) } 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 51d2ce92fa..6f3a0a1e98 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,7 +16,6 @@ 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 @@ -70,6 +69,9 @@ abstract class AbsMessageItem : BaseEventItem() { @EpoxyAttribute var avatarCallback: TimelineEventController.AvatarCallback? = null + @EpoxyAttribute + var readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null + private val _avatarClickListener = DebouncedClickListener(View.OnClickListener { avatarCallback?.onAvatarClicked(informationData) }) @@ -77,6 +79,9 @@ abstract class AbsMessageItem : BaseEventItem() { avatarCallback?.onMemberNameClicked(informationData) }) + private val _readReceiptsClickListener = DebouncedClickListener(View.OnClickListener { + readReceiptsCallback?.onReadReceiptsClicked(informationData) + }) var reactionClickListener: ReactionButton.ReactedListener = object : ReactionButton.ReactedListener { override fun onReacted(reactionButton: ReactionButton) { @@ -124,7 +129,7 @@ abstract class AbsMessageItem : BaseEventItem() { holder.memberNameView.setOnLongClickListener(null) } - holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer) + holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener) if (!shouldShowReactionAtBottom() || informationData.orderedReactionList.isNullOrEmpty()) { holder.reactionWrapper?.isVisible = false 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 679bfbba6a..d46b2a8db3 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,7 +17,6 @@ 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 diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt index b18a665d6b..c4b5f042d4 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt @@ -23,6 +23,7 @@ import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R import im.vector.riotx.core.ui.views.ReadReceiptsView +import im.vector.riotx.core.utils.DebouncedClickListener import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController @@ -45,6 +46,13 @@ abstract class NoticeItem : BaseEventItem() { return@OnLongClickListener baseCallback?.onEventLongClicked(informationData, null, it) == true } + @EpoxyAttribute + var readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null + + private val _readReceiptsClickListener = DebouncedClickListener(View.OnClickListener { + readReceiptsCallback?.onReadReceiptsClicked(informationData) + }) + override fun bind(holder: Holder) { super.bind(holder) holder.noticeTextView.text = noticeText @@ -56,7 +64,7 @@ abstract class NoticeItem : BaseEventItem() { holder.avatarImageView ) holder.view.setOnLongClickListener(longClickListener) - holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer) + holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener) } override fun getViewType() = STUB_ID 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 2f6a432004..a34c98744f 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 @@ -24,7 +24,7 @@ import im.vector.riotx.core.extensions.localDateTime import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.utils.isSingleEmoji import im.vector.riotx.features.home.getColorFromUserId -import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDateFormatter +import im.vector.riotx.core.date.VectorDateFormatter 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 @@ -35,7 +35,7 @@ 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 session: Session, - private val timelineDateFormatter: TimelineDateFormatter, + private val dateFormatter: VectorDateFormatter, private val colorProvider: ColorProvider) { fun create(event: TimelineEvent, nextEvent: TimelineEvent?): MessageInformationData { @@ -55,7 +55,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses || (nextEvent?.root?.getClearType() != EventType.MESSAGE && nextEvent?.root?.getClearType() != EventType.ENCRYPTED) || isNextMessageReceivedMoreThanOneHourAgo - val time = timelineDateFormatter.formatMessageHour(date) + val time = dateFormatter.formatMessageHour(date) val avatarUrl = event.senderAvatar val memberName = event.getDisambiguatedDisplayName() val formattedMemberName = span(memberName) { @@ -79,12 +79,14 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses hasBeenEdited = event.hasBeenEdited(), hasPendingEdits = event.annotations?.editSummary?.localEchos?.any() ?: false, readReceipts = event.readReceipts + .asSequence() .filter { it.user.userId != session.myUserId } .map { ReadReceiptData(it.user.userId, it.user.avatarUrl, it.user.displayName, it.originServerTs) } + .toList() ) } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt index 6af6234142..38f15974f3 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt @@ -29,13 +29,13 @@ import im.vector.riotx.core.resources.DateProvider import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter -import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDateFormatter +import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.features.home.room.detail.timeline.helper.senderName import me.gujun.android.span.span import javax.inject.Inject class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatter: NoticeEventFormatter, - private val timelineDateFormatter: TimelineDateFormatter, + private val dateFormatter: VectorDateFormatter, private val colorProvider: ColorProvider, private val stringProvider: StringProvider, private val avatarRenderer: AvatarRenderer) { @@ -94,7 +94,7 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte val currentDate = DateProvider.currentLocalDateTime() val isSameDay = date.toLocalDate() == currentDate.toLocalDate() latestFormattedEvent = if (latestEvent.root.isEncrypted() - && latestEvent.root.mxDecryptionResult == null) { + && latestEvent.root.mxDecryptionResult == null) { stringProvider.getString(R.string.encrypted_message) } else if (latestEvent.root.getClearType() == EventType.MESSAGE) { val senderName = latestEvent.senderName() ?: latestEvent.root.senderId @@ -117,10 +117,9 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte } } latestEventTime = if (isSameDay) { - timelineDateFormatter.formatMessageHour(date) + dateFormatter.formatMessageHour(date) } else { - //TODO: change this - timelineDateFormatter.formatMessageDay(date) + dateFormatter.formatMessageDay(date) } } return RoomSummaryItem_() diff --git a/vector/src/main/res/layout/item_display_read_receipt.xml b/vector/src/main/res/layout/item_display_read_receipt.xml new file mode 100644 index 0000000000..cf3c9a2662 --- /dev/null +++ b/vector/src/main/res/layout/item_display_read_receipt.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + \ No newline at end of file From 21deb2551d952b64aee1f86a2160a8b9370eb420 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 12 Aug 2019 17:59:07 +0200 Subject: [PATCH 08/14] Read receipts: handle read receipts set on filtered events + let BottomSheet takes a snapshot instead of being live. --- .../session/room/timeline/TimelineService.kt | 6 +- .../session/room/timeline/TimelineSettings.kt | 39 ++++ .../database/helper/ChunkEntityHelper.kt | 2 +- .../mapper/ReadReceiptsSummaryMapper.kt | 5 +- .../database/mapper/TimelineEventMapper.kt | 13 +- .../model/ReadReceiptsSummaryEntity.kt | 6 + .../query/ReadReceiptsSummaryEntityQueries.kt | 8 + .../internal/session/room/RoomFactory.kt | 2 +- .../session/room/timeline/DefaultTimeline.kt | 178 +++++++++++++----- .../room/timeline/DefaultTimelineService.kt | 10 +- .../session/sync/ReadReceiptHandler.kt | 2 +- .../vector/riotx/core/di/ViewModelModule.kt | 6 - .../home/room/detail/RoomDetailFragment.kt | 4 +- .../home/room/detail/RoomDetailViewModel.kt | 24 +-- .../DisplayReadReceiptsBottomSheet.kt | 34 ++-- .../DisplayReadReceiptsController.kt | 49 ++--- .../DisplayReadReceiptsViewModel.kt | 63 ------- .../DisplayReadReceiptsViewState.kt | 33 ---- .../timeline/TimelineEventController.kt | 3 +- .../timeline/factory/MessageItemFactory.kt | 25 +-- .../timeline/factory/TimelineItemFactory.kt | 27 +-- .../timeline/format/NoticeEventFormatter.kt | 14 +- .../detail/timeline/item/AbsMessageItem.kt | 2 +- .../room/detail/timeline/item/NoticeItem.kt | 2 +- .../util/MessageInformationDataFactory.kt | 5 +- 25 files changed, 277 insertions(+), 285 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineSettings.kt delete mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewModel.kt delete mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewState.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt index 5a4838adcd..fdf99bd22c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt @@ -25,12 +25,12 @@ interface TimelineService { /** * Instantiate a [Timeline] with an optional initial eventId, to be used with permalink. - * You can filter the type you want to grab with the allowedTypes param. + * You can also configure some settings with the [settings] param. * @param eventId the optional initial eventId. - * @param allowedTypes the optional filter types + * @param settings settings to configure the timeline. * @return the instantiated timeline */ - fun createTimeline(eventId: String?, allowedTypes: List? = null): Timeline + fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline fun getTimeLineEvent(eventId: String): TimelineEvent? diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineSettings.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineSettings.kt new file mode 100644 index 0000000000..219c23ebeb --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineSettings.kt @@ -0,0 +1,39 @@ +/* + * 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.api.session.room.timeline + +/** + * Data class holding setting values for a [Timeline] instance. + */ +data class TimelineSettings( + /** + * The initial number of events to retrieve from cache. You might get less events if you don't have loaded enough yet. + */ + val initialSize: Int, + /** + * A flag to filter edit events + */ + val filterEdits: Boolean = false, + /** + * A flag to filter by types. It should be used with [allowedTypes] field + */ + val filterTypes: Boolean = false, + /** + * If [filterTypes] is true, the list of types allowed by the list. + */ + val allowedTypes: List = emptyList() +) 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 a541e8cf39..69065f5171 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 @@ -140,7 +140,7 @@ internal fun ChunkEntity.add(roomId: String, val senderId = event.senderId ?: "" val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst() - ?: ReadReceiptsSummaryEntity(eventId) + ?: ReadReceiptsSummaryEntity(eventId, roomId) // Update RR for the sender of a new message with a dummy one 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 index b7cdabfc4d..1f53d1b410 100644 --- 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 @@ -28,7 +28,10 @@ import javax.inject.Inject internal class ReadReceiptsSummaryMapper @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration) { - fun map(readReceiptsSummaryEntity: ReadReceiptsSummaryEntity): List { + fun map(readReceiptsSummaryEntity: ReadReceiptsSummaryEntity?): List { + if (readReceiptsSummaryEntity == null) { + return emptyList() + } return Realm.getInstance(realmConfiguration).use { realm -> val readReceipts = readReceiptsSummaryEntity.readReceipts readReceipts 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 5290692c3e..42365d7e18 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,15 +17,18 @@ 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.model.ReadReceipt import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.internal.database.model.TimelineEventEntity import javax.inject.Inject -internal class TimelineEventMapper @Inject constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper){ - - fun map(timelineEventEntity: TimelineEventEntity): TimelineEvent { +internal class TimelineEventMapper @Inject constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper) { + fun map(timelineEventEntity: TimelineEventEntity, correctedReadReceipts: List? = null): TimelineEvent { + val readReceipts = correctedReadReceipts ?: timelineEventEntity.readReceipts?.let { + readReceiptsSummaryMapper.map(it) + } return TimelineEvent( root = timelineEventEntity.root?.asDomain() ?: Event("", timelineEventEntity.eventId), @@ -35,9 +38,7 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS senderName = timelineEventEntity.senderName, isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName, senderAvatar = timelineEventEntity.senderAvatar, - readReceipts = timelineEventEntity.readReceipts?.let { - readReceiptsSummaryMapper.map(it) - } ?: emptyList() + readReceipts = readReceipts ?: emptyList() ) } 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 index e0fe970fe9..56e8938caa 100644 --- 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 @@ -18,14 +18,20 @@ package im.vector.matrix.android.internal.database.model import io.realm.RealmList import io.realm.RealmObject +import io.realm.RealmResults +import io.realm.annotations.LinkingObjects import io.realm.annotations.PrimaryKey internal open class ReadReceiptsSummaryEntity( @PrimaryKey var eventId: String = "", + var roomId: String = "", var readReceipts: RealmList = RealmList() ) : RealmObject() { + @LinkingObjects("readReceipts") + val timelineEvent: RealmResults? = null + companion object } \ No newline at end of file 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 index d04ced119c..0c3d7d8eb1 100644 --- 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 @@ -26,3 +26,11 @@ internal fun ReadReceiptsSummaryEntity.Companion.where(realm: Realm, eventId: St return realm.where() .equalTo(ReadReceiptsSummaryEntityFields.EVENT_ID, eventId) } + +internal fun ReadReceiptsSummaryEntity.Companion.whereInRoom(realm: Realm, roomId: String?): RealmQuery { + val query = realm.where() + if (roomId != null) { + query.equalTo(ReadReceiptsSummaryEntityFields.ROOM_ID, roomId) + } + return query +} 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 cf2627b0b4..e68e4282ff 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 @@ -65,7 +65,7 @@ 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, timelineEventMapper) + val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, cryptoService, paginationTask, timelineEventMapper, readReceiptsSummaryMapper) 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) 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 921c65eabe..059b24150f 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 @@ -16,25 +16,47 @@ package im.vector.matrix.android.internal.session.room.timeline +import android.util.SparseArray import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.room.model.ReadReceipt 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.session.room.timeline.TimelineSettings import im.vector.matrix.android.api.util.CancelableBag +import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper 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.ChunkEntity +import im.vector.matrix.android.internal.database.model.ChunkEntityFields +import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.query.* +import im.vector.matrix.android.internal.database.model.EventEntityFields +import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity +import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntityFields +import im.vector.matrix.android.internal.database.model.RoomEntity +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.findAllInRoomWithSendStates +import im.vector.matrix.android.internal.database.query.findIncludingEvent +import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom +import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.database.query.whereInRoom import im.vector.matrix.android.internal.task.TaskConstraints import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.Debouncer import im.vector.matrix.android.internal.util.createBackgroundHandler import im.vector.matrix.android.internal.util.createUIHandler -import io.realm.* +import io.realm.OrderedCollectionChangeSet +import io.realm.OrderedRealmCollectionChangeListener +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.RealmQuery +import io.realm.RealmResults +import io.realm.Sort import timber.log.Timber import java.util.* import java.util.concurrent.atomic.AtomicBoolean @@ -43,10 +65,11 @@ import kotlin.collections.ArrayList import kotlin.collections.HashMap -private const val INITIAL_LOAD_SIZE = 30 private const val MIN_FETCHING_COUNT = 30 private const val DISPLAY_INDEX_UNKNOWN = Int.MIN_VALUE +private const val EDIT_FILTER_LIKE = "{*\"m.relates_to\"*\"rel_type\":*\"m.replace\"*}" + internal class DefaultTimeline( private val roomId: String, private val initialEventId: String? = null, @@ -56,7 +79,8 @@ internal class DefaultTimeline( private val paginationTask: PaginationTask, private val cryptoService: CryptoService, private val timelineEventMapper: TimelineEventMapper, - private val allowedTypes: List? + private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, + private val settings: TimelineSettings ) : Timeline { private companion object { @@ -79,6 +103,11 @@ internal class DefaultTimeline( private val debouncer = Debouncer(mainHandler) private lateinit var liveEvents: RealmResults + private lateinit var eventRelations: RealmResults + private var hiddenReadReceipts: RealmResults? = null + private val correctedReadReceiptsEventByIndex = SparseArray() + private val correctedReadReceiptsByEvent = HashMap>() + private var roomEntity: RoomEntity? = null private var prevDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN @@ -92,7 +121,6 @@ internal class DefaultTimeline( private val timelineID = UUID.randomUUID().toString() - private lateinit var eventRelations: RealmResults private val eventDecryptor = TimelineEventDecryptor(realmConfiguration, timelineID, cryptoService) @@ -132,9 +160,9 @@ internal class DefaultTimeline( val eventEntity = results[index] eventEntity?.eventId?.let { eventId -> builtEventsIdMap[eventId]?.let { builtIndex -> - //Update the relation of existing event + //Update an existing event builtEvents[builtIndex]?.let { te -> - builtEvents[builtIndex] = timelineEventMapper.map(eventEntity) + builtEvents[builtIndex] = timelineEventMapper.map(eventEntity, correctedReadReceiptsByEvent[te.root.eventId]) hasChanged = true } } @@ -164,32 +192,54 @@ internal class DefaultTimeline( postSnapshot() } -// private val newSessionListener = object : NewSessionListener { -// override fun onNewSession(roomId: String?, senderKey: String, sessionId: String) { -// if (roomId == this@DefaultTimeline.roomId) { -// Timber.v("New session id detected for this room") -// BACKGROUND_HANDLER.post { -// val realm = backgroundRealm.get() -// var hasChange = false -// builtEvents.forEachIndexed { index, timelineEvent -> -// if (timelineEvent.isEncrypted()) { -// val eventContent = timelineEvent.root.content.toModel() -// if (eventContent?.sessionId == sessionId -// && (timelineEvent.root.mClearEvent == null || timelineEvent.root.mCryptoError != null)) { -// //we need to rebuild this event -// EventEntity.where(realm, eventId = timelineEvent.root.eventId!!).findFirst()?.let { -// //builtEvents[index] = timelineEventFactory.create(it, realm) -// hasChange = true -// } -// } -// } -// } -// if (hasChange) postSnapshot() -// } -// } -// } -// -// } + private val hiddenReadReceiptsListener = OrderedRealmCollectionChangeListener> { collection, changeSet -> + var hasChange = false + changeSet.deletions.forEach { + val eventId = correctedReadReceiptsEventByIndex[it] + val timelineEvent = liveEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, eventId).findFirst() + builtEventsIdMap[eventId]?.let { builtIndex -> + builtEvents[builtIndex]?.let { te -> + builtEvents[builtIndex] = te.copy(readReceipts = readReceiptsSummaryMapper.map(timelineEvent?.readReceipts)) + hasChange = true + } + } + } + correctedReadReceiptsEventByIndex.clear() + correctedReadReceiptsByEvent.clear() + val loadedReadReceipts = collection.where().greaterThan("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.DISPLAY_INDEX}", prevDisplayIndex).findAll() + loadedReadReceipts.forEachIndexed { index, summary -> + val timelineEvent = summary?.timelineEvent?.firstOrNull() + val displayIndex = timelineEvent?.root?.displayIndex + if (displayIndex != null) { + val firstDisplayedEvent = liveEvents.where() + .sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING) + .lessThan(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, displayIndex) + .findFirst() + + if (firstDisplayedEvent != null) { + correctedReadReceiptsEventByIndex.put(index, firstDisplayedEvent.eventId) + correctedReadReceiptsByEvent.getOrPut(firstDisplayedEvent.eventId, { + readReceiptsSummaryMapper.map(firstDisplayedEvent.readReceipts).toMutableList() + }).addAll( + readReceiptsSummaryMapper.map(summary) + ) + } + } + } + if (correctedReadReceiptsByEvent.isNotEmpty()) { + correctedReadReceiptsByEvent.forEach { (eventId, correctedReadReceipts) -> + builtEventsIdMap[eventId]?.let { builtIndex -> + builtEvents[builtIndex]?.let { te -> + builtEvents[builtIndex] = te.copy(readReceipts = correctedReadReceipts) + hasChange = true + } + } + } + } + if (hasChange) { + postSnapshot() + } + } // Public methods ****************************************************************************** @@ -236,15 +286,23 @@ internal class DefaultTimeline( } liveEvents = buildEventQuery(realm) + .filterEventsWithSettings() .sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING) .findAllAsync() .also { it.addChangeListener(eventsChangeListener) } - isReady.set(true) - eventRelations = EventAnnotationsSummaryEntity.whereInRoom(realm, roomId) .findAllAsync() .also { it.addChangeListener(relationsListener) } + + hiddenReadReceipts = ReadReceiptsSummaryEntity.whereInRoom(realm, roomId) + .isNotEmpty(ReadReceiptsSummaryEntityFields.TIMELINE_EVENT) + .isNotEmpty(ReadReceiptsSummaryEntityFields.READ_RECEIPTS.`$`) + .filterReceiptsWithSettings() + .findAllAsync() + .also { it.addChangeListener(hiddenReadReceiptsListener) } + + isReady.set(true) } } } @@ -257,6 +315,7 @@ internal class DefaultTimeline( cancelableBag.cancel() roomEntity?.sendingTimelineEvents?.removeAllChangeListeners() eventRelations.removeAllChangeListeners() + hiddenReadReceipts?.removeAllChangeListeners() liveEvents.removeAllChangeListeners() backgroundRealm.getAndSet(null).also { it.close() @@ -274,7 +333,7 @@ internal class DefaultTimeline( private fun hasMoreInCache(direction: Timeline.Direction): Boolean { return Realm.getInstance(realmConfiguration).use { localRealm -> val timelineEventEntity = buildEventQuery(localRealm).findFirst(direction) - ?: return false + ?: return false if (direction == Timeline.Direction.FORWARDS) { if (findCurrentChunk(localRealm)?.isLastForward == true) { return false @@ -331,7 +390,9 @@ internal class DefaultTimeline( val sendingEvents = ArrayList() if (hasReachedEnd(Timeline.Direction.FORWARDS)) { roomEntity?.sendingTimelineEvents - ?.filter { allowedTypes?.contains(it.root?.type) ?: false } + ?.where() + ?.filterEventsWithSettings() + ?.findAll() ?.forEach { sendingEvents.add(timelineEventMapper.map(it)) } @@ -380,7 +441,7 @@ internal class DefaultTimeline( if (initialEventId != null && shouldFetchInitialEvent) { fetchEvent(initialEventId) } else { - val count = Math.min(INITIAL_LOAD_SIZE, liveEvents.size) + val count = Math.min(settings.initialSize, liveEvents.size) if (isLive) { paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count) } else { @@ -397,9 +458,9 @@ internal class DefaultTimeline( private fun executePaginationTask(direction: Timeline.Direction, limit: Int) { val token = getTokenLive(direction) ?: return val params = PaginationTask.Params(roomId = roomId, - from = token, - direction = direction.toPaginationDirection(), - limit = limit) + from = token, + direction = direction.toPaginationDirection(), + limit = limit) Timber.v("Should fetch $limit items $direction") cancelableBag += paginationTask @@ -465,10 +526,11 @@ internal class DefaultTimeline( nextDisplayIndex = offsetIndex + 1 } offsetResults.forEach { eventEntity -> + val timelineEvent = timelineEventMapper.map(eventEntity) if (timelineEvent.isEncrypted() - && timelineEvent.root.mxDecryptionResult == null) { + && timelineEvent.root.mxDecryptionResult == null) { timelineEvent.root.eventId?.let { eventDecryptor.requestDecryption(it) } } @@ -500,7 +562,6 @@ internal class DefaultTimeline( .greaterThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex) } return offsetQuery - .filterAllowedTypes() .limit(count) .findAll() } @@ -559,14 +620,35 @@ internal class DefaultTimeline( } else { sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.ASCENDING) } - .filterAllowedTypes() + .filterEventsWithSettings() .findFirst() } - private fun RealmQuery.filterAllowedTypes(): RealmQuery { - if (allowedTypes != null) { - `in`(TimelineEventEntityFields.ROOT.TYPE, allowedTypes.toTypedArray()) + private fun RealmQuery.filterEventsWithSettings(): RealmQuery { + if (settings.filterTypes) { + `in`(TimelineEventEntityFields.ROOT.TYPE, settings.allowedTypes.toTypedArray()) } + if (settings.filterEdits) { + not().like(TimelineEventEntityFields.ROOT.CONTENT, EDIT_FILTER_LIKE) + } + return this + } + + /** + * We are looking for receipts related to filtered events. So, it's the opposite of [filterEventsWithSettings] method. + */ + private fun RealmQuery.filterReceiptsWithSettings(): RealmQuery { + beginGroup() + if (settings.filterTypes) { + not().`in`("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.TYPE}", settings.allowedTypes.toTypedArray()) + } + if (settings.filterTypes && settings.filterEdits) { + or() + } + if (settings.filterEdits) { + like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", EDIT_FILTER_LIKE) + } + endGroup() return this } } 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 94fc433a1e..a2a802742a 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 @@ -23,7 +23,9 @@ import im.vector.matrix.android.api.session.crypto.CryptoService 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.api.session.room.timeline.TimelineSettings import im.vector.matrix.android.internal.database.RealmLiveData +import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper 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 @@ -38,10 +40,11 @@ internal class DefaultTimelineService @Inject constructor(private val roomId: St private val contextOfEventTask: GetContextOfEventTask, private val cryptoService: CryptoService, private val paginationTask: PaginationTask, - private val timelineEventMapper: TimelineEventMapper + private val timelineEventMapper: TimelineEventMapper, + private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper ) : TimelineService { - override fun createTimeline(eventId: String?, allowedTypes: List?): Timeline { + override fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline { return DefaultTimeline(roomId, eventId, monarchy.realmConfiguration, @@ -50,7 +53,8 @@ internal class DefaultTimelineService @Inject constructor(private val roomId: St paginationTask, cryptoService, timelineEventMapper, - allowedTypes + readReceiptsSummaryMapper, + settings ) } 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 5098e824f9..7c752e4905 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 @@ -62,7 +62,7 @@ internal class ReadReceiptHandler @Inject constructor() { val readReceiptSummaries = ArrayList() for ((eventId, receiptDict) in content) { val userIdsDict = receiptDict[READ_KEY] ?: continue - val readReceiptsSummary = ReadReceiptsSummaryEntity(eventId = eventId) + val readReceiptsSummary = ReadReceiptsSummaryEntity(eventId = eventId, roomId = roomId) for ((userId, paramsDict) in userIdsDict) { val ts = paramsDict[TIMESTAMP_KEY] ?: 0.0 diff --git a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt index c2c86cad46..b7d63e67a7 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt @@ -43,8 +43,6 @@ import im.vector.riotx.features.home.room.detail.RoomDetailViewModel import im.vector.riotx.features.home.room.detail.RoomDetailViewModel_AssistedFactory import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel_AssistedFactory -import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsViewModel -import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsViewModel_AssistedFactory import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsViewModel import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsViewModel_AssistedFactory import im.vector.riotx.features.home.room.detail.timeline.action.MessageMenuViewModel @@ -197,8 +195,4 @@ interface ViewModelModule { @Binds fun bindPushGatewaysViewModelFactory(factory: PushGatewaysViewModel_AssistedFactory): PushGatewaysViewModel.Factory - - @Binds - fun bindDisplayReadReceiptsViewModel(factory: DisplayReadReceiptsViewModel_AssistedFactory): DisplayReadReceiptsViewModel.Factory - } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 544f05cd99..04dd1150b3 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -817,8 +817,8 @@ class RoomDetailFragment : }) } - override fun onReadReceiptsClicked(informationData: MessageInformationData) { - DisplayReadReceiptsBottomSheet.newInstance(roomDetailArgs.roomId, informationData) + override fun onReadReceiptsClicked(readReceipts: List) { + DisplayReadReceiptsBottomSheet.newInstance(readReceipts) .show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS") } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 2bb7327d57..2bc08bd042 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -43,6 +43,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.getFileUrl import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.matrix.android.api.session.room.timeline.TimelineSettings import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.matrix.rx.rx @@ -73,12 +74,13 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private val roomId = initialState.roomId private val eventId = initialState.eventId private val displayedEventsObservable = BehaviorRelay.create() - private val allowedTypes = if (userPreferencesProvider.shouldShowHiddenEvents()) { - TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES + private val timelineSettings = if (userPreferencesProvider.shouldShowHiddenEvents()) { + TimelineSettings(30, false, true, TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES) } else { - TimelineDisplayableEvents.DISPLAYABLE_TYPES + TimelineSettings(30, true, true, TimelineDisplayableEvents.DISPLAYABLE_TYPES) } - private var timeline = room.createTimeline(eventId, allowedTypes) + + private var timeline = room.createTimeline(eventId, timelineSettings) // Slot to keep a pending action during permission request var pendingAction: RoomDetailActions? = null @@ -137,7 +139,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private fun handleTombstoneEvent(action: RoomDetailActions.HandleTombstoneEvent) { val tombstoneContent = action.event.getClearContent().toModel() - ?: return + ?: return val roomId = tombstoneContent.replacementRoom ?: "" val isRoomJoined = session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN @@ -283,7 +285,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro //is original event a reply? val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel()?.relatesTo?.inReplyTo?.eventId - ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId + ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId if (inReplyTo != null) { //TODO check if same content? room.getTimeLineEvent(inReplyTo)?.let { @@ -292,12 +294,12 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } else { val messageContent: MessageContent? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() - ?: state.sendMode.timelineEvent.root.getClearContent().toModel() + ?: state.sendMode.timelineEvent.root.getClearContent().toModel() val existingBody = messageContent?.body ?: "" if (existingBody != action.text) { room.editTextMessage(state.sendMode.timelineEvent.root.eventId - ?: "", messageContent?.type - ?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown) + ?: "", messageContent?.type + ?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown) } else { Timber.w("Same message content, do not send edition") } @@ -312,7 +314,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is SendMode.QUOTE -> { val messageContent: MessageContent? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() - ?: state.sendMode.timelineEvent.root.getClearContent().toModel() + ?: state.sendMode.timelineEvent.root.getClearContent().toModel() val textMsg = messageContent?.body val finalText = legacyRiotQuoteText(textMsg, action.text) @@ -550,7 +552,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } else { // change timeline timeline.dispose() - timeline = room.createTimeline(targetEventId, allowedTypes) + timeline = room.createTimeline(targetEventId, timelineSettings) timeline.start() withState { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt index 572954f106..b8c1519f4c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt @@ -16,8 +16,8 @@ package im.vector.riotx.features.home.room.detail.readreceipts -import android.annotation.SuppressLint import android.os.Bundle +import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -27,31 +27,31 @@ import butterknife.BindView import butterknife.ButterKnife import com.airbnb.epoxy.EpoxyRecyclerView import com.airbnb.mvrx.MvRx -import com.airbnb.mvrx.fragmentViewModel -import com.airbnb.mvrx.withState -import im.vector.riotx.EmojiCompatFontProvider +import com.airbnb.mvrx.args import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent -import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs import im.vector.riotx.features.home.room.detail.timeline.action.VectorBaseBottomSheetDialogFragment -import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionBottomSheet -import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData +import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData +import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.* import javax.inject.Inject +@Parcelize +data class DisplayReadReceiptArgs( + val readReceipts: List +) : Parcelable + /** * Bottom sheet displaying list of read receipts for a given event ordered by descending timestamp */ class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() { - private val viewModel: DisplayReadReceiptsViewModel by fragmentViewModel() - - @Inject lateinit var displayReadReceiptsViewModelFactory: DisplayReadReceiptsViewModel.Factory @Inject lateinit var epoxyController: DisplayReadReceiptsController @BindView(R.id.bottom_sheet_display_reactions_list) lateinit var epoxyRecyclerView: EpoxyRecyclerView + private val displayReadReceiptArgs: DisplayReadReceiptArgs by args() override fun injectWith(screenComponent: ScreenComponent) { screenComponent.inject(this) @@ -70,20 +70,18 @@ class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() { LinearLayout.VERTICAL) epoxyRecyclerView.addItemDecoration(dividerItemDecoration) bottomSheetTitle.text = getString(R.string.read_receipts_list) + epoxyController.setData(displayReadReceiptArgs.readReceipts) } - - override fun invalidate() = withState(viewModel) { - epoxyController.setData(it) + override fun invalidate() { + // we are not using state for this one as it's static } companion object { - fun newInstance(roomId: String, informationData: MessageInformationData): DisplayReadReceiptsBottomSheet { + fun newInstance(readReceipts: List): DisplayReadReceiptsBottomSheet { val args = Bundle() - val parcelableArgs = TimelineEventFragmentArgs( - informationData.eventId, - roomId, - informationData + val parcelableArgs = DisplayReadReceiptArgs( + readReceipts ) args.putParcelable(MvRx.KEY_ARG, parcelableArgs) return DisplayReadReceiptsBottomSheet().apply { arguments = args } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsController.kt index 2c2f9f498c..51c150be83 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsController.kt @@ -17,55 +17,32 @@ package im.vector.riotx.features.home.room.detail.readreceipts import com.airbnb.epoxy.TypedEpoxyController -import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.Incomplete -import com.airbnb.mvrx.Success import im.vector.matrix.android.api.session.Session -import im.vector.riotx.R import im.vector.riotx.core.date.VectorDateFormatter -import im.vector.riotx.core.resources.StringProvider -import im.vector.riotx.core.ui.list.genericFooterItem -import im.vector.riotx.core.ui.list.genericLoaderItem import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData import javax.inject.Inject /** * Epoxy controller for read receipt event list */ class DisplayReadReceiptsController @Inject constructor(private val dateFormatter: VectorDateFormatter, - private val stringProvider: StringProvider, private val session: Session, private val avatarRender: AvatarRenderer) - : TypedEpoxyController() { + : TypedEpoxyController>() { - override fun buildModels(state: DisplayReadReceiptsViewState) { - when (state.readReceipts) { - is Incomplete -> { - genericLoaderItem { - id("loading") - } - } - is Fail -> { - genericFooterItem { - id("failure") - text(stringProvider.getString(R.string.unknown_error)) - } - } - is Success -> { - state.readReceipts()?.forEach { - val timestamp = dateFormatter.formatRelativeDateTime(it.originServerTs) - DisplayReadReceiptItem_() - .id(it.user.userId) - .userId(it.user.userId) - .avatarUrl(it.user.avatarUrl) - .name(it.user.displayName) - .avatarRenderer(avatarRender) - .timestamp(timestamp) - .addIf(session.myUserId != it.user.userId, this) - } - } + override fun buildModels(readReceipts: List) { + readReceipts.forEach { + val timestamp = dateFormatter.formatRelativeDateTime(it.timestamp) + DisplayReadReceiptItem_() + .id(it.userId) + .userId(it.userId) + .avatarUrl(it.avatarUrl) + .name(it.displayName) + .avatarRenderer(avatarRender) + .timestamp(timestamp) + .addIf(session.myUserId != it.userId, this) } } - } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewModel.kt deleted file mode 100644 index 8423ba4ac6..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewModel.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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.riotx.features.home.room.detail.readreceipts - -import com.airbnb.mvrx.* -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject -import im.vector.matrix.android.api.session.Session -import im.vector.matrix.rx.RxRoom -import im.vector.riotx.core.platform.VectorViewModel - -/** - * Used to display the list of read receipts to a given event - */ -class DisplayReadReceiptsViewModel @AssistedInject constructor(@Assisted initialState: DisplayReadReceiptsViewState, - private val session: Session -) : VectorViewModel(initialState) { - - private val roomId = initialState.roomId - private val eventId = initialState.eventId - private val room = session.getRoom(roomId) - ?: throw IllegalStateException("Shouldn't use this ViewModel without a room") - - @AssistedInject.Factory - interface Factory { - fun create(initialState: DisplayReadReceiptsViewState): DisplayReadReceiptsViewModel - } - - companion object : MvRxViewModelFactory { - - override fun create(viewModelContext: ViewModelContext, state: DisplayReadReceiptsViewState): DisplayReadReceiptsViewModel? { - val fragment: DisplayReadReceiptsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.displayReadReceiptsViewModelFactory.create(state) - } - } - - init { - observeEventAnnotationSummaries() - } - - private fun observeEventAnnotationSummaries() { - RxRoom(room) - .liveEventReadReceipts(eventId) - .execute { - copy(readReceipts = it) - } - } - -} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewState.kt deleted file mode 100644 index 68952b998e..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewState.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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.riotx.features.home.room.detail.readreceipts - -import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState -import com.airbnb.mvrx.Uninitialized -import im.vector.matrix.android.api.session.room.model.ReadReceipt -import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs - -data class DisplayReadReceiptsViewState( - val eventId: String, - val roomId: String, - val readReceipts: Async> = Uninitialized -) : MvRxState { - - constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId) - -} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index 28a3d100d1..3c212d6129 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt @@ -38,6 +38,7 @@ import im.vector.riotx.features.home.room.detail.timeline.item.DaySeparatorItem import im.vector.riotx.features.home.room.detail.timeline.item.DaySeparatorItem_ import im.vector.riotx.features.home.room.detail.timeline.item.MergedHeaderItem import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData +import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.riotx.features.media.ImageContentRenderer import im.vector.riotx.features.media.VideoContentRenderer import org.threeten.bp.LocalDateTime @@ -79,7 +80,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } interface ReadReceiptsCallback { - fun onReadReceiptsClicked(informationData: MessageInformationData) + fun onReadReceiptsClicked(readReceipts: List) } interface UrlClickCallback { 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 71e7627d63..f3a93a8d6d 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 @@ -84,7 +84,7 @@ class MessageItemFactory @Inject constructor( private val imageContentRenderer: ImageContentRenderer, private val messageInformationDataFactory: MessageInformationDataFactory, private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder, - private val userPreferencesProvider: UserPreferencesProvider) { + private val noticeItemFactory: NoticeItemFactory) { fun create(event: TimelineEvent, @@ -109,27 +109,8 @@ class MessageItemFactory @Inject constructor( if (messageContent.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()) { - //These are just for debug to display hidden event, they should be filtered out in normal mode - val informationData = MessageInformationData( - eventId = event.root.eventId ?: "?", - senderId = event.root.senderId ?: "", - sendState = event.root.sendState, - time = "", - avatarUrl = event.senderAvatar(), - memberName = "", - showInformation = false - ) - return NoticeItem_() - .avatarRenderer(avatarRenderer) - .informationData(informationData) - .noticeText("{ \"type\": ${event.root.getClearType()} }") - .highlighted(highlight) - .baseCallback(callback) - } else { - return BlankItem_() - } + // This is an edit event, we should it when debugging as a notice event + return noticeItemFactory.create(event, highlight, callback) } // val all = event.root.toContent() // val ev = all.toModel() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index 43197d8bad..b1ae595ea0 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -25,6 +25,7 @@ import im.vector.riotx.features.home.room.detail.timeline.TimelineEventControlle import im.vector.riotx.features.home.room.detail.timeline.helper.senderAvatar import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem_ +import im.vector.riotx.features.home.room.detail.timeline.util.MessageInformationDataFactory import timber.log.Timber import javax.inject.Inject @@ -33,8 +34,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me private val encryptedItemFactory: EncryptedItemFactory, private val noticeItemFactory: NoticeItemFactory, private val defaultItemFactory: DefaultItemFactory, - private val roomCreateItemFactory: RoomCreateItemFactory, - private val avatarRenderer: AvatarRenderer) { + private val roomCreateItemFactory: RoomCreateItemFactory) { fun create(event: TimelineEvent, nextEvent: TimelineEvent?, @@ -53,7 +53,9 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.STATE_HISTORY_VISIBILITY, EventType.CALL_INVITE, EventType.CALL_HANGUP, - EventType.CALL_ANSWER -> noticeItemFactory.create(event, highlight, callback) + EventType.CALL_ANSWER, + EventType.REACTION, + EventType.REDACTION -> noticeItemFactory.create(event, highlight, callback) // State room create EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback) // Crypto @@ -70,24 +72,9 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me // Unhandled event types (yet) EventType.STATE_ROOM_THIRD_PARTY_INVITE, EventType.STICKER -> defaultItemFactory.create(event, highlight) - else -> { - //These are just for debug to display hidden event, they should be filtered out in normal mode - val informationData = MessageInformationData( - eventId = event.root.eventId ?: "?", - senderId = event.root.senderId ?: "", - sendState = event.root.sendState, - time = "", - avatarUrl = event.senderAvatar(), - memberName = "", - showInformation = false - ) - NoticeItem_() - .avatarRenderer(avatarRenderer) - .informationData(informationData) - .noticeText("{ \"type\": ${event.root.getClearType()} }") - .highlighted(highlight) - .baseCallback(callback) + Timber.v("Type ${event.root.getClearType()} not handled") + null } } } catch (e: Exception) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index 2b1a263328..05ce7a9c19 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -22,7 +22,6 @@ 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.* import im.vector.matrix.android.api.session.room.model.call.CallInviteContent -import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotx.R import im.vector.riotx.core.resources.StringProvider @@ -42,6 +41,9 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin EventType.CALL_INVITE, EventType.CALL_HANGUP, EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) + EventType.MESSAGE, + EventType.REACTION, + EventType.REDACTION -> formatDebug(timelineEvent.root) else -> { Timber.v("Type $type not handled by this formatter") null @@ -66,6 +68,10 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin } } + private fun formatDebug(event: Event): CharSequence? { + return "{ \"type\": ${event.getClearType()} }" + } + private fun formatRoomNameEvent(event: Event, senderName: String?): CharSequence? { val content = event.getClearContent().toModel() ?: return null return if (!TextUtils.isEmpty(content.name)) { @@ -90,7 +96,7 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?): CharSequence? { val historyVisibility = event.getClearContent().toModel()?.historyVisibility - ?: return null + ?: return null val formattedVisibility = when (historyVisibility) { RoomHistoryVisibility.SHARED -> stringProvider.getString(R.string.notice_room_visibility_shared) @@ -140,7 +146,7 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin stringProvider.getString(R.string.notice_display_name_removed, event.senderId, prevEventContent?.displayName) else -> stringProvider.getString(R.string.notice_display_name_changed_from, - event.senderId, prevEventContent?.displayName, eventContent?.displayName) + event.senderId, prevEventContent?.displayName, eventContent?.displayName) } displayText.append(displayNameText) } @@ -167,7 +173,7 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin when { eventContent.thirdPartyInvite != null -> stringProvider.getString(R.string.notice_room_third_party_registered_invite, - targetDisplayName, eventContent.thirdPartyInvite?.displayName) + targetDisplayName, eventContent.thirdPartyInvite?.displayName) TextUtils.equals(event.stateKey, selfUserId) -> stringProvider.getString(R.string.notice_room_invite_you, senderDisplayName) event.stateKey.isNullOrEmpty() -> 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 6f3a0a1e98..a394f47124 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 @@ -80,7 +80,7 @@ abstract class AbsMessageItem : BaseEventItem() { }) private val _readReceiptsClickListener = DebouncedClickListener(View.OnClickListener { - readReceiptsCallback?.onReadReceiptsClicked(informationData) + readReceiptsCallback?.onReadReceiptsClicked(informationData.readReceipts) }) var reactionClickListener: ReactionButton.ReactedListener = object : ReactionButton.ReactedListener { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt index c4b5f042d4..51a7b0ce38 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt @@ -50,7 +50,7 @@ abstract class NoticeItem : BaseEventItem() { var readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null private val _readReceiptsClickListener = DebouncedClickListener(View.OnClickListener { - readReceiptsCallback?.onReadReceiptsClicked(informationData) + readReceiptsCallback?.onReadReceiptsClicked(informationData.readReceipts) }) override fun bind(holder: Holder) { 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 a34c98744f..a00dd3fa9f 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 @@ -52,15 +52,14 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses addDaySeparator || event.senderAvatar != nextEvent?.senderAvatar || event.getDisambiguatedDisplayName() != nextEvent?.getDisambiguatedDisplayName() - || (nextEvent?.root?.getClearType() != EventType.MESSAGE && nextEvent?.root?.getClearType() != EventType.ENCRYPTED) + || (nextEvent.root.getClearType() != EventType.MESSAGE && nextEvent.root.getClearType() != EventType.ENCRYPTED) || isNextMessageReceivedMoreThanOneHourAgo val time = dateFormatter.formatMessageHour(date) val avatarUrl = event.senderAvatar val memberName = event.getDisambiguatedDisplayName() val formattedMemberName = span(memberName) { - textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId - ?: "")) + textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId ?: "")) } return MessageInformationData( From 06dcf75a3211e6d057684715ccb5d8d18d2a68b3 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 13 Aug 2019 12:06:49 +0200 Subject: [PATCH 09/14] Read receipts: fix not appearing RR --- .../mapper/ReadReceiptsSummaryMapper.kt | 4 - .../database/mapper/TimelineEventMapper.kt | 12 +- .../session/room/read/DefaultReadService.kt | 2 + .../session/room/timeline/DefaultTimeline.kt | 112 ++++----------- .../room/timeline/DefaultTimelineService.kt | 4 +- .../timeline/TimelineHiddenReadReceipts.kt | 130 ++++++++++++++++++ .../session/sync/ReadReceiptHandler.kt | 4 +- 7 files changed, 170 insertions(+), 98 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt 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 index 1f53d1b410..9fa9fc011d 100644 --- 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 @@ -17,7 +17,6 @@ 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.ReadReceiptEntityFields 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 @@ -40,9 +39,6 @@ internal class ReadReceiptsSummaryMapper @Inject constructor(@SessionDatabase pr ?: return@mapNotNull null ReadReceipt(user.asDomain(), it.originServerTs.toLong()) } - .sortedByDescending { - it.originServerTs - } } } 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 42365d7e18..84c860a83d 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 @@ -26,9 +26,11 @@ import javax.inject.Inject internal class TimelineEventMapper @Inject constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper) { fun map(timelineEventEntity: TimelineEventEntity, correctedReadReceipts: List? = null): TimelineEvent { - val readReceipts = correctedReadReceipts ?: timelineEventEntity.readReceipts?.let { - readReceiptsSummaryMapper.map(it) - } + val readReceipts = correctedReadReceipts ?: timelineEventEntity.readReceipts + ?.let { + readReceiptsSummaryMapper.map(it) + } + return TimelineEvent( root = timelineEventEntity.root?.asDomain() ?: Event("", timelineEventEntity.eventId), @@ -38,7 +40,9 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS senderName = timelineEventEntity.senderName, isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName, senderAvatar = timelineEventEntity.senderAvatar, - readReceipts = readReceipts ?: emptyList() + readReceipts = readReceipts?.sortedByDescending { + it.originServerTs + } ?: emptyList() ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt index 3df872bf4b..470668ef08 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt @@ -96,6 +96,8 @@ internal class DefaultReadService @Inject constructor(private val roomId: String return Transformations.map(liveEntity) { realmResults -> realmResults.firstOrNull()?.let { readReceiptsSummaryMapper.map(it) + }?.sortedByDescending { + it.originServerTs } ?: emptyList() } } 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 059b24150f..f961f9d53b 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 @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.session.room.timeline -import android.util.SparseArray import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.events.model.EventType @@ -34,8 +33,6 @@ import im.vector.matrix.android.internal.database.model.ChunkEntityFields import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntityFields -import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity -import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntityFields import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields @@ -68,7 +65,7 @@ import kotlin.collections.HashMap private const val MIN_FETCHING_COUNT = 30 private const val DISPLAY_INDEX_UNKNOWN = Int.MIN_VALUE -private const val EDIT_FILTER_LIKE = "{*\"m.relates_to\"*\"rel_type\":*\"m.replace\"*}" +internal const val EDIT_FILTER_LIKE = "{*\"m.relates_to\"*\"rel_type\":*\"m.replace\"*}" internal class DefaultTimeline( private val roomId: String, @@ -79,9 +76,9 @@ internal class DefaultTimeline( private val paginationTask: PaginationTask, private val cryptoService: CryptoService, private val timelineEventMapper: TimelineEventMapper, - private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, - private val settings: TimelineSettings -) : Timeline { + private val settings: TimelineSettings, + private val hiddenReadReceipts: TimelineHiddenReadReceipts +) : Timeline, TimelineHiddenReadReceipts.Delegate { private companion object { val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD") @@ -104,9 +101,6 @@ internal class DefaultTimeline( private lateinit var liveEvents: RealmResults private lateinit var eventRelations: RealmResults - private var hiddenReadReceipts: RealmResults? = null - private val correctedReadReceiptsEventByIndex = SparseArray() - private val correctedReadReceiptsByEvent = HashMap>() private var roomEntity: RoomEntity? = null @@ -118,10 +112,8 @@ internal class DefaultTimeline( private val backwardsPaginationState = AtomicReference(PaginationState()) private val forwardsPaginationState = AtomicReference(PaginationState()) - private val timelineID = UUID.randomUUID().toString() - private val eventDecryptor = TimelineEventDecryptor(realmConfiguration, timelineID, cryptoService) private val eventsChangeListener = OrderedRealmCollectionChangeListener> { results, changeSet -> @@ -162,7 +154,7 @@ internal class DefaultTimeline( builtEventsIdMap[eventId]?.let { builtIndex -> //Update an existing event builtEvents[builtIndex]?.let { te -> - builtEvents[builtIndex] = timelineEventMapper.map(eventEntity, correctedReadReceiptsByEvent[te.root.eventId]) + builtEvents[builtIndex] = timelineEventMapper.map(eventEntity, hiddenReadReceipts.correctedReadReceipts(te.root.eventId)) hasChanged = true } } @@ -192,56 +184,8 @@ internal class DefaultTimeline( postSnapshot() } - private val hiddenReadReceiptsListener = OrderedRealmCollectionChangeListener> { collection, changeSet -> - var hasChange = false - changeSet.deletions.forEach { - val eventId = correctedReadReceiptsEventByIndex[it] - val timelineEvent = liveEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, eventId).findFirst() - builtEventsIdMap[eventId]?.let { builtIndex -> - builtEvents[builtIndex]?.let { te -> - builtEvents[builtIndex] = te.copy(readReceipts = readReceiptsSummaryMapper.map(timelineEvent?.readReceipts)) - hasChange = true - } - } - } - correctedReadReceiptsEventByIndex.clear() - correctedReadReceiptsByEvent.clear() - val loadedReadReceipts = collection.where().greaterThan("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.DISPLAY_INDEX}", prevDisplayIndex).findAll() - loadedReadReceipts.forEachIndexed { index, summary -> - val timelineEvent = summary?.timelineEvent?.firstOrNull() - val displayIndex = timelineEvent?.root?.displayIndex - if (displayIndex != null) { - val firstDisplayedEvent = liveEvents.where() - .sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING) - .lessThan(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, displayIndex) - .findFirst() - if (firstDisplayedEvent != null) { - correctedReadReceiptsEventByIndex.put(index, firstDisplayedEvent.eventId) - correctedReadReceiptsByEvent.getOrPut(firstDisplayedEvent.eventId, { - readReceiptsSummaryMapper.map(firstDisplayedEvent.readReceipts).toMutableList() - }).addAll( - readReceiptsSummaryMapper.map(summary) - ) - } - } - } - if (correctedReadReceiptsByEvent.isNotEmpty()) { - correctedReadReceiptsByEvent.forEach { (eventId, correctedReadReceipts) -> - builtEventsIdMap[eventId]?.let { builtIndex -> - builtEvents[builtIndex]?.let { te -> - builtEvents[builtIndex] = te.copy(readReceipts = correctedReadReceipts) - hasChange = true - } - } - } - } - if (hasChange) { - postSnapshot() - } - } - -// Public methods ****************************************************************************** + // Public methods ****************************************************************************** override fun paginate(direction: Timeline.Direction, count: Int) { BACKGROUND_HANDLER.post { @@ -295,12 +239,7 @@ internal class DefaultTimeline( .findAllAsync() .also { it.addChangeListener(relationsListener) } - hiddenReadReceipts = ReadReceiptsSummaryEntity.whereInRoom(realm, roomId) - .isNotEmpty(ReadReceiptsSummaryEntityFields.TIMELINE_EVENT) - .isNotEmpty(ReadReceiptsSummaryEntityFields.READ_RECEIPTS.`$`) - .filterReceiptsWithSettings() - .findAllAsync() - .also { it.addChangeListener(hiddenReadReceiptsListener) } + hiddenReadReceipts.start(realm, liveEvents, this) isReady.set(true) } @@ -315,8 +254,8 @@ internal class DefaultTimeline( cancelableBag.cancel() roomEntity?.sendingTimelineEvents?.removeAllChangeListeners() eventRelations.removeAllChangeListeners() - hiddenReadReceipts?.removeAllChangeListeners() liveEvents.removeAllChangeListeners() + hiddenReadReceipts.dispose() backgroundRealm.getAndSet(null).also { it.close() } @@ -328,6 +267,22 @@ internal class DefaultTimeline( return hasMoreInCache(direction) || !hasReachedEnd(direction) } + // TimelineHiddenReadReceipts.Delegate + + override fun rebuildEvent(eventId: String, readReceipts: List): Boolean { + return builtEventsIdMap[eventId]?.let { builtIndex -> + //Update the relation of existing event + builtEvents[builtIndex]?.let { te -> + builtEvents[builtIndex] = te.copy(readReceipts = readReceipts) + true + } + } ?: false + } + + override fun onReadReceiptsUpdated() { + postSnapshot() + } + // Private methods ***************************************************************************** private fun hasMoreInCache(direction: Timeline.Direction): Boolean { @@ -608,7 +563,7 @@ internal class DefaultTimeline( debouncer.debounce("post_snapshot", runnable, 50) } -// Extension methods *************************************************************************** + // Extension methods *************************************************************************** private fun Timeline.Direction.toPaginationDirection(): PaginationDirection { return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS @@ -634,23 +589,6 @@ internal class DefaultTimeline( return this } - /** - * We are looking for receipts related to filtered events. So, it's the opposite of [filterEventsWithSettings] method. - */ - private fun RealmQuery.filterReceiptsWithSettings(): RealmQuery { - beginGroup() - if (settings.filterTypes) { - not().`in`("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.TYPE}", settings.allowedTypes.toTypedArray()) - } - if (settings.filterTypes && settings.filterEdits) { - or() - } - if (settings.filterEdits) { - like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", EDIT_FILTER_LIKE) - } - endGroup() - return this - } } private data class PaginationState( 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 a2a802742a..82058a9156 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 @@ -53,8 +53,8 @@ internal class DefaultTimelineService @Inject constructor(private val roomId: St paginationTask, cryptoService, timelineEventMapper, - readReceiptsSummaryMapper, - settings + settings, + TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings) ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt new file mode 100644 index 0000000000..db34d240cf --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt @@ -0,0 +1,130 @@ +/* + * 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.session.room.timeline + +import android.util.SparseArray +import im.vector.matrix.android.api.session.room.model.ReadReceipt +import im.vector.matrix.android.api.session.room.timeline.TimelineSettings +import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper +import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity +import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntityFields +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.whereInRoom +import io.realm.OrderedRealmCollectionChangeListener +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.RealmResults + +internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, + private val roomId: String, + private val settings: TimelineSettings) { + + interface Delegate { + fun rebuildEvent(eventId: String, readReceipts: List): Boolean + fun onReadReceiptsUpdated() + } + + private val correctedReadReceiptsEventByIndex = SparseArray() + private val correctedReadReceiptsByEvent = HashMap>() + + private lateinit var hiddenReadReceipts: RealmResults + private lateinit var liveEvents: RealmResults + private lateinit var delegate: Delegate + + private val hiddenReadReceiptsListener = OrderedRealmCollectionChangeListener> { collection, changeSet -> + var hasChange = false + changeSet.deletions.forEach { + val eventId = correctedReadReceiptsEventByIndex[it] + val timelineEvent = liveEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, eventId).findFirst() + val readReceipts = readReceiptsSummaryMapper.map(timelineEvent?.readReceipts) + hasChange = hasChange || delegate.rebuildEvent(eventId, readReceipts) + } + correctedReadReceiptsEventByIndex.clear() + correctedReadReceiptsByEvent.clear() + hiddenReadReceipts.forEachIndexed { index, summary -> + val timelineEvent = summary?.timelineEvent?.firstOrNull() + val displayIndex = timelineEvent?.root?.displayIndex + if (displayIndex != null) { + val firstDisplayedEvent = liveEvents.where() + .lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, displayIndex) + .findFirst() + + if (firstDisplayedEvent != null) { + correctedReadReceiptsEventByIndex.put(index, firstDisplayedEvent.eventId) + correctedReadReceiptsByEvent.getOrPut(firstDisplayedEvent.eventId, { + readReceiptsSummaryMapper.map(firstDisplayedEvent.readReceipts).toMutableList() + }).addAll( + readReceiptsSummaryMapper.map(summary) + ) + } + } + } + if (correctedReadReceiptsByEvent.isNotEmpty()) { + correctedReadReceiptsByEvent.forEach { (eventId, correctedReadReceipts) -> + val sortedReadReceipts = correctedReadReceipts.sortedByDescending { + it.originServerTs + } + hasChange = hasChange || delegate.rebuildEvent(eventId, sortedReadReceipts) + } + } + if (hasChange) { + delegate.onReadReceiptsUpdated() + } + } + + + fun start(realm: Realm, liveEvents: RealmResults, delegate: Delegate) { + this.liveEvents = liveEvents + this.delegate = delegate + this.hiddenReadReceipts = ReadReceiptsSummaryEntity.whereInRoom(realm, roomId) + .isNotEmpty(ReadReceiptsSummaryEntityFields.TIMELINE_EVENT) + .isNotEmpty(ReadReceiptsSummaryEntityFields.READ_RECEIPTS.`$`) + .filterReceiptsWithSettings() + .findAllAsync() + .also { it.addChangeListener(hiddenReadReceiptsListener) } + } + + fun dispose() { + this.hiddenReadReceipts?.removeAllChangeListeners() + } + + fun correctedReadReceipts(eventId: String?): List? { + return correctedReadReceiptsByEvent[eventId] + } + + + /** + * We are looking for receipts related to filtered events. So, it's the opposite of [filterEventsWithSettings] method. + */ + private fun RealmQuery.filterReceiptsWithSettings(): RealmQuery { + beginGroup() + if (settings.filterTypes) { + not().`in`("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.TYPE}", settings.allowedTypes.toTypedArray()) + } + if (settings.filterTypes && settings.filterEdits) { + or() + } + if (settings.filterEdits) { + like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", EDIT_FILTER_LIKE) + } + endGroup() + return this + } + + +} \ No newline at end of file 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 7c752e4905..e61e81dd16 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 @@ -78,7 +78,9 @@ internal class ReadReceiptHandler @Inject constructor() { for ((eventId, receiptDict) in content) { val userIdsDict = receiptDict[READ_KEY] ?: continue val readReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst() - ?: realm.createObject(ReadReceiptsSummaryEntity::class.java, eventId) + ?: realm.createObject(ReadReceiptsSummaryEntity::class.java, eventId).apply { + this.roomId = roomId + } for ((userId, paramsDict) in userIdsDict) { val ts = paramsDict[TIMESTAMP_KEY] ?: 0.0 From b9cfda23b6497957a9bf6551139815a684040354 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 13 Aug 2019 15:06:00 +0200 Subject: [PATCH 10/14] Read receipts: just juste invisible on hidden avatars, to have a bigger touch zone --- .../im/vector/riotx/core/ui/views/ReadReceiptsView.kt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt index 6293e22bd7..44d1ee6f70 100644 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt @@ -18,8 +18,10 @@ package im.vector.riotx.core.ui.views import android.content.Context import android.util.AttributeSet +import android.view.View import android.widget.ImageView import android.widget.LinearLayout +import androidx.core.view.isInvisible import androidx.core.view.isVisible import butterknife.ButterKnife import im.vector.riotx.R @@ -56,24 +58,23 @@ class ReadReceiptsView @JvmOverloads constructor( for (index in 0 until MAX_RECEIPT_DISPLAYED) { val receiptData = readReceipts.getOrNull(index) if (receiptData == null) { - receiptAvatars[index].isVisible = false + receiptAvatars[index].visibility = View.INVISIBLE } else { - receiptAvatars[index].isVisible = true + receiptAvatars[index].visibility = View.VISIBLE avatarRenderer.render(receiptData.avatarUrl, receiptData.userId, receiptData.displayName, receiptAvatars[index]) } } if (readReceipts.size > MAX_RECEIPT_DISPLAYED) { - receiptMore.isVisible = true + receiptMore.visibility = View.VISIBLE receiptMore.text = context.getString( R.string.x_plus, readReceipts.size - MAX_RECEIPT_DISPLAYED ) } else { - receiptMore.isVisible = false + receiptMore.visibility = View.GONE } } else { isVisible = false } - } } From 4e8dc724397de731294eeec4ca149f36a9977554 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 13 Aug 2019 15:17:04 +0200 Subject: [PATCH 11/14] Update CHANGES --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index b8fa02b925..3f83d97765 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,7 +2,7 @@ Changes in RiotX 0.4.0 (2019-XX-XX) =================================================== Features: - - + - Display read receipts in timeline Improvements: - From d3827b86735639e4cef78b1bf8e9ed463b62f0c0 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 14 Aug 2019 10:51:09 +0200 Subject: [PATCH 12/14] Read receipts: branch settings to show/hide them --- .../session/room/timeline/TimelineSettings.kt | 7 +++++- .../database/mapper/TimelineEventMapper.kt | 15 +++++++----- .../internal/database/query/FilterContent.kt | 23 ++++++++++++++++++ .../session/room/timeline/DefaultTimeline.kt | 24 ++++++++++++------- .../room/timeline/DefaultTimelineService.kt | 1 - .../timeline/TimelineHiddenReadReceipts.kt | 15 ++++++++---- .../core/resources/UserPreferencesProvider.kt | 5 ++++ .../home/room/detail/RoomDetailViewModel.kt | 6 ++--- 8 files changed, 73 insertions(+), 23 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineSettings.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineSettings.kt index 219c23ebeb..992cad41ca 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineSettings.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineSettings.kt @@ -35,5 +35,10 @@ data class TimelineSettings( /** * If [filterTypes] is true, the list of types allowed by the list. */ - val allowedTypes: List = emptyList() + val allowedTypes: List = emptyList(), + /** + * If true, will build read receipts for each event. + */ + val buildReadReceipts: Boolean = true + ) 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 84c860a83d..fe98ebfb5b 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 @@ -25,12 +25,15 @@ import javax.inject.Inject internal class TimelineEventMapper @Inject constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper) { - fun map(timelineEventEntity: TimelineEventEntity, correctedReadReceipts: List? = null): TimelineEvent { - val readReceipts = correctedReadReceipts ?: timelineEventEntity.readReceipts - ?.let { - readReceiptsSummaryMapper.map(it) - } - + fun map(timelineEventEntity: TimelineEventEntity, buildReadReceipts: Boolean = true, correctedReadReceipts: List? = null): TimelineEvent { + val readReceipts = if (buildReadReceipts) { + correctedReadReceipts ?: timelineEventEntity.readReceipts + ?.let { + readReceiptsSummaryMapper.map(it) + } + } else { + null + } return TimelineEvent( root = timelineEventEntity.root?.asDomain() ?: Event("", timelineEventEntity.eventId), diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.kt new file mode 100644 index 0000000000..9e6261c6b7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.kt @@ -0,0 +1,23 @@ +/* + * 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 + +internal object FilterContent { + + internal const val EDIT_TYPE = "{*\"m.relates_to\"*\"rel_type\":*\"m.replace\"*}" + +} \ No newline at end of file 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 f961f9d53b..03f5da6e6f 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 @@ -25,7 +25,6 @@ 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.TimelineSettings import im.vector.matrix.android.api.util.CancelableBag -import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper 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.ChunkEntity @@ -36,6 +35,7 @@ import im.vector.matrix.android.internal.database.model.EventEntityFields import im.vector.matrix.android.internal.database.model.RoomEntity 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.FilterContent import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates import im.vector.matrix.android.internal.database.query.findIncludingEvent import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom @@ -65,8 +65,6 @@ import kotlin.collections.HashMap private const val MIN_FETCHING_COUNT = 30 private const val DISPLAY_INDEX_UNKNOWN = Int.MIN_VALUE -internal const val EDIT_FILTER_LIKE = "{*\"m.relates_to\"*\"rel_type\":*\"m.replace\"*}" - internal class DefaultTimeline( private val roomId: String, private val initialEventId: String? = null, @@ -154,7 +152,7 @@ internal class DefaultTimeline( builtEventsIdMap[eventId]?.let { builtIndex -> //Update an existing event builtEvents[builtIndex]?.let { te -> - builtEvents[builtIndex] = timelineEventMapper.map(eventEntity, hiddenReadReceipts.correctedReadReceipts(te.root.eventId)) + builtEvents[builtIndex] = buildTimelineEvent(eventEntity) hasChanged = true } } @@ -239,7 +237,9 @@ internal class DefaultTimeline( .findAllAsync() .also { it.addChangeListener(relationsListener) } - hiddenReadReceipts.start(realm, liveEvents, this) + if (settings.buildReadReceipts) { + hiddenReadReceipts.start(realm, liveEvents, this) + } isReady.set(true) } @@ -255,7 +255,9 @@ internal class DefaultTimeline( roomEntity?.sendingTimelineEvents?.removeAllChangeListeners() eventRelations.removeAllChangeListeners() liveEvents.removeAllChangeListeners() - hiddenReadReceipts.dispose() + if (settings.buildReadReceipts) { + hiddenReadReceipts.dispose() + } backgroundRealm.getAndSet(null).also { it.close() } @@ -482,7 +484,7 @@ internal class DefaultTimeline( } offsetResults.forEach { eventEntity -> - val timelineEvent = timelineEventMapper.map(eventEntity) + val timelineEvent = buildTimelineEvent(eventEntity) if (timelineEvent.isEncrypted() && timelineEvent.root.mxDecryptionResult == null) { @@ -500,6 +502,12 @@ internal class DefaultTimeline( return offsetResults.size } + private fun buildTimelineEvent(eventEntity: TimelineEventEntity) = timelineEventMapper.map( + timelineEventEntity = eventEntity, + buildReadReceipts = settings.buildReadReceipts, + correctedReadReceipts = hiddenReadReceipts.correctedReadReceipts(eventEntity.eventId) + ) + /** * This has to be called on TimelineThread as it access realm live results */ @@ -584,7 +592,7 @@ internal class DefaultTimeline( `in`(TimelineEventEntityFields.ROOT.TYPE, settings.allowedTypes.toTypedArray()) } if (settings.filterEdits) { - not().like(TimelineEventEntityFields.ROOT.CONTENT, EDIT_FILTER_LIKE) + not().like(TimelineEventEntityFields.ROOT.CONTENT, FilterContent.EDIT_TYPE) } return this } 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 82058a9156..441b062030 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 @@ -27,7 +27,6 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineSettings import im.vector.matrix.android.internal.database.RealmLiveData import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper 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 import im.vector.matrix.android.internal.task.TaskExecutor diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt index db34d240cf..5678677d32 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt @@ -24,6 +24,7 @@ import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntit import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntityFields 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.FilterContent import im.vector.matrix.android.internal.database.query.whereInRoom import io.realm.OrderedRealmCollectionChangeListener import io.realm.Realm @@ -48,11 +49,13 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu private val hiddenReadReceiptsListener = OrderedRealmCollectionChangeListener> { collection, changeSet -> var hasChange = false + // Deletion here means we don't have any readReceipts for the given hidden events changeSet.deletions.forEach { val eventId = correctedReadReceiptsEventByIndex[it] val timelineEvent = liveEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, eventId).findFirst() + // We are rebuilding the corresponding event with only his own RR val readReceipts = readReceiptsSummaryMapper.map(timelineEvent?.readReceipts) - hasChange = hasChange || delegate.rebuildEvent(eventId, readReceipts) + hasChange = delegate.rebuildEvent(eventId, readReceipts) || hasChange } correctedReadReceiptsEventByIndex.clear() correctedReadReceiptsByEvent.clear() @@ -60,10 +63,12 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu val timelineEvent = summary?.timelineEvent?.firstOrNull() val displayIndex = timelineEvent?.root?.displayIndex if (displayIndex != null) { + // Then we are looking for the first displayable event after the hidden one val firstDisplayedEvent = liveEvents.where() .lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, displayIndex) .findFirst() + // If we find one, we should if (firstDisplayedEvent != null) { correctedReadReceiptsEventByIndex.put(index, firstDisplayedEvent.eventId) correctedReadReceiptsByEvent.getOrPut(firstDisplayedEvent.eventId, { @@ -79,7 +84,7 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu val sortedReadReceipts = correctedReadReceipts.sortedByDescending { it.originServerTs } - hasChange = hasChange || delegate.rebuildEvent(eventId, sortedReadReceipts) + hasChange = delegate.rebuildEvent(eventId, sortedReadReceipts) || hasChange } } if (hasChange) { @@ -91,6 +96,8 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu fun start(realm: Realm, liveEvents: RealmResults, delegate: Delegate) { this.liveEvents = liveEvents this.delegate = delegate + // We are looking for read receipts set on hidden events. + // We only accept those with a timelineEvent (so coming from pagination/sync). this.hiddenReadReceipts = ReadReceiptsSummaryEntity.whereInRoom(realm, roomId) .isNotEmpty(ReadReceiptsSummaryEntityFields.TIMELINE_EVENT) .isNotEmpty(ReadReceiptsSummaryEntityFields.READ_RECEIPTS.`$`) @@ -109,7 +116,7 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu /** - * We are looking for receipts related to filtered events. So, it's the opposite of [filterEventsWithSettings] method. + * We are looking for receipts related to filtered events. So, it's the opposite of [DefaultTimeline.filterEventsWithSettings] method. */ private fun RealmQuery.filterReceiptsWithSettings(): RealmQuery { beginGroup() @@ -120,7 +127,7 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu or() } if (settings.filterEdits) { - like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", EDIT_FILTER_LIKE) + like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", FilterContent.EDIT_TYPE) } endGroup() return this diff --git a/vector/src/main/java/im/vector/riotx/core/resources/UserPreferencesProvider.kt b/vector/src/main/java/im/vector/riotx/core/resources/UserPreferencesProvider.kt index 0d2c30a5b9..2d411f302f 100644 --- a/vector/src/main/java/im/vector/riotx/core/resources/UserPreferencesProvider.kt +++ b/vector/src/main/java/im/vector/riotx/core/resources/UserPreferencesProvider.kt @@ -24,4 +24,9 @@ class UserPreferencesProvider @Inject constructor(private val vectorPreferences: fun shouldShowHiddenEvents(): Boolean { return vectorPreferences.shouldShowHiddenEvents() } + + fun shouldShowReadReceipts(): Boolean { + return vectorPreferences.showReadReceipts() + } + } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 12dace1b67..1cd8cc4a41 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -67,7 +67,7 @@ import java.util.concurrent.TimeUnit class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: RoomDetailViewState, - userPreferencesProvider: UserPreferencesProvider, + private val userPreferencesProvider: UserPreferencesProvider, private val vectorPreferences: VectorPreferences, private val session: Session ) : VectorViewModel(initialState) { @@ -77,9 +77,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private val eventId = initialState.eventId private val displayedEventsObservable = BehaviorRelay.create() private val timelineSettings = if (userPreferencesProvider.shouldShowHiddenEvents()) { - TimelineSettings(30, false, true, TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES) + TimelineSettings(30, false, true, TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES, userPreferencesProvider.shouldShowReadReceipts()) } else { - TimelineSettings(30, true, true, TimelineDisplayableEvents.DISPLAYABLE_TYPES) + TimelineSettings(30, true, true, TimelineDisplayableEvents.DISPLAYABLE_TYPES, userPreferencesProvider.shouldShowReadReceipts()) } private var timeline = room.createTimeline(eventId, timelineSettings) From 501474b7207040a6cf11f61fbe4206096c182445 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 14 Aug 2019 14:53:40 +0200 Subject: [PATCH 13/14] Fix code quality issues --- .../internal/session/room/RoomFactory.kt | 20 +++++++++++++++++-- .../timeline/TimelineHiddenReadReceipts.kt | 15 ++++++++------ .../riotx/core/date/VectorDateFormatter.kt | 7 ++++++- 3 files changed, 33 insertions(+), 9 deletions(-) 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 e68e4282ff..d982763abf 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 @@ -65,13 +65,29 @@ 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, timelineEventMapper, readReceiptsSummaryMapper) + val timelineService = DefaultTimelineService(roomId, + monarchy, + taskExecutor, + contextOfEventTask, + cryptoService, + paginationTask, + timelineEventMapper, + readReceiptsSummaryMapper + ) 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, readReceiptsSummaryMapper, 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/TimelineHiddenReadReceipts.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt index 5678677d32..e42bf23017 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt @@ -52,7 +52,10 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu // Deletion here means we don't have any readReceipts for the given hidden events changeSet.deletions.forEach { val eventId = correctedReadReceiptsEventByIndex[it] - val timelineEvent = liveEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, eventId).findFirst() + val timelineEvent = liveEvents.where() + .equalTo(TimelineEventEntityFields.EVENT_ID, eventId) + .findFirst() + // We are rebuilding the corresponding event with only his own RR val readReceipts = readReceiptsSummaryMapper.map(timelineEvent?.readReceipts) hasChange = delegate.rebuildEvent(eventId, readReceipts) || hasChange @@ -71,11 +74,11 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu // If we find one, we should if (firstDisplayedEvent != null) { correctedReadReceiptsEventByIndex.put(index, firstDisplayedEvent.eventId) - correctedReadReceiptsByEvent.getOrPut(firstDisplayedEvent.eventId, { - readReceiptsSummaryMapper.map(firstDisplayedEvent.readReceipts).toMutableList() - }).addAll( - readReceiptsSummaryMapper.map(summary) - ) + correctedReadReceiptsByEvent + .getOrPut(firstDisplayedEvent.eventId, { + ArrayList(readReceiptsSummaryMapper.map(firstDisplayedEvent.readReceipts)) + }) + .addAll(readReceiptsSummaryMapper.map(summary)) } } } diff --git a/vector/src/main/java/im/vector/riotx/core/date/VectorDateFormatter.kt b/vector/src/main/java/im/vector/riotx/core/date/VectorDateFormatter.kt index 540400d570..f8e8d61220 100644 --- a/vector/src/main/java/im/vector/riotx/core/date/VectorDateFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/core/date/VectorDateFormatter.kt @@ -46,7 +46,12 @@ class VectorDateFormatter @Inject constructor(private val context: Context, if (time == null) { return "" } - return DateUtils.getRelativeDateTimeString(context, time, DateUtils.DAY_IN_MILLIS, 2 * DateUtils.DAY_IN_MILLIS, DateUtils.FORMAT_SHOW_WEEKDAY).toString() + return DateUtils.getRelativeDateTimeString(context, + time, + DateUtils.DAY_IN_MILLIS, + 2 * DateUtils.DAY_IN_MILLIS, + DateUtils.FORMAT_SHOW_WEEKDAY + ).toString() } } \ No newline at end of file From fd74e3dfb1f5471cb458891c394b206ea77cb4b9 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 19 Aug 2019 14:08:15 +0200 Subject: [PATCH 14/14] Read receipts: clean code after review --- CHANGES.md | 2 +- .../internal/database/query/FilterContent.kt | 2 +- .../room/timeline/TimelineHiddenReadReceipts.kt | 17 +++++++++++++++-- .../res/layout/item_display_read_receipt.xml | 11 +++++------ .../res/layout/item_simple_reaction_info.xml | 6 +++--- 5 files changed, 25 insertions(+), 13 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 3f83d97765..682c176b98 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,7 +2,7 @@ Changes in RiotX 0.4.0 (2019-XX-XX) =================================================== Features: - - Display read receipts in timeline + - Display read receipts in timeline (#81) Improvements: - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.kt index 9e6261c6b7..92608a1f42 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.kt @@ -18,6 +18,6 @@ package im.vector.matrix.android.internal.database.query internal object FilterContent { - internal const val EDIT_TYPE = "{*\"m.relates_to\"*\"rel_type\":*\"m.replace\"*}" + internal const val EDIT_TYPE = """{*"m.relates_to"*"rel_type":*"m.replace"*}""" } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt index e42bf23017..5658210302 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt @@ -31,6 +31,11 @@ import io.realm.Realm import io.realm.RealmQuery import io.realm.RealmResults +/** + * This class is responsible for handling the read receipts for hidden events (check [TimelineSettings] to see filtering). + * When an hidden event has read receipts, we want to transfer these read receipts on the first older displayed event. + * It has to be used in [DefaultTimeline] and we should call the [start] and [dispose] methods to properly handle realm subscription. + */ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, private val roomId: String, private val settings: TimelineSettings) { @@ -95,7 +100,9 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu } } - + /** + * Start the realm query subscription. Has to be called on an HandlerThread + */ fun start(realm: Realm, liveEvents: RealmResults, delegate: Delegate) { this.liveEvents = liveEvents this.delegate = delegate @@ -109,10 +116,16 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu .also { it.addChangeListener(hiddenReadReceiptsListener) } } + /** + * Dispose the realm query subscription. Has to be called on an HandlerThread + */ fun dispose() { - this.hiddenReadReceipts?.removeAllChangeListeners() + this.hiddenReadReceipts.removeAllChangeListeners() } + /** + * Return the current corrected [ReadReceipt] list for an event, or null + */ fun correctedReadReceipts(eventId: String?): List? { return correctedReadReceiptsByEvent[eventId] } diff --git a/vector/src/main/res/layout/item_display_read_receipt.xml b/vector/src/main/res/layout/item_display_read_receipt.xml index cf3c9a2662..9b4072ab76 100644 --- a/vector/src/main/res/layout/item_display_read_receipt.xml +++ b/vector/src/main/res/layout/item_display_read_receipt.xml @@ -3,11 +3,10 @@ @@ -35,9 +35,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:lines="1" - android:textColor="?android:textColorSecondary" + android:textColor="?riotx_text_secondary" android:textSize="12sp" tools:text="10:44" /> - \ No newline at end of file diff --git a/vector/src/main/res/layout/item_simple_reaction_info.xml b/vector/src/main/res/layout/item_simple_reaction_info.xml index 0458b17126..06f94fc8cf 100644 --- a/vector/src/main/res/layout/item_simple_reaction_info.xml +++ b/vector/src/main/res/layout/item_simple_reaction_info.xml @@ -2,11 +2,10 @@