First step in handling read receipts

This commit is contained in:
ganfra 2019-08-08 14:32:11 +02:00
parent 7fef063e15
commit b4ce8748cb
23 changed files with 422 additions and 126 deletions

View File

@ -16,8 +16,9 @@


package im.vector.matrix.android.api.session.room.model package im.vector.matrix.android.api.session.room.model


import im.vector.matrix.android.api.session.user.model.User

data class ReadReceipt( data class ReadReceipt(
val userId: String, val user: User,
val eventId: String,
val originServerTs: Long val originServerTs: Long
) )

View File

@ -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.EventType
import im.vector.matrix.android.api.session.events.model.toModel 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.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.MessageContent
import im.vector.matrix.android.api.session.room.model.message.isReply import im.vector.matrix.android.api.session.room.model.message.isReply
import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply
@ -37,7 +38,8 @@ data class TimelineEvent(
val senderName: String?, val senderName: String?,
val isUniqueDisplayName: Boolean, val isUniqueDisplayName: Boolean,
val senderAvatar: String?, val senderAvatar: String?,
val annotations: EventAnnotationsSummary? = null val annotations: EventAnnotationsSummary? = null,
val readReceipts: List<ReadReceipt> = emptyList()
) { ) {


val metadata = HashMap<String, Any>() val metadata = HashMap<String, Any>()
@ -65,8 +67,8 @@ data class TimelineEvent(
"$name (${root.senderId})" "$name (${root.senderId})"
} }
} }
?: root.senderId ?: root.senderId
?: "" ?: ""
} }


/** /**
@ -94,7 +96,7 @@ fun TimelineEvent.hasBeenEdited() = annotations?.editSummary != null
* Get last MessageContent, after a possible edition * Get last MessageContent, after a possible edition
*/ */
fun TimelineEvent.getLastMessageContent(): MessageContent? = annotations?.editSummary?.aggregatedContent?.toModel() fun TimelineEvent.getLastMessageContent(): MessageContent? = annotations?.editSummary?.aggregatedContent?.toModel()
?: root.getClearContent().toModel() ?: root.getClearContent().toModel()




fun TimelineEvent.getTextEditableContent(): String? { fun TimelineEvent.getTextEditableContent(): String? {

View File

@ -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.mapper.toEntity
import im.vector.matrix.android.internal.database.model.ChunkEntity 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.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.TimelineEventEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields 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.find
@ -133,6 +135,23 @@ internal fun ChunkEntity.add(roomId: String,
} }


val localId = TimelineEventEntity.nextId(realm) 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 { val eventEntity = TimelineEventEntity(localId).also {
it.root = event.toEntity(roomId).apply { it.root = event.toEntity(roomId).apply {
this.stateIndex = currentStateIndex this.stateIndex = currentStateIndex
@ -140,9 +159,10 @@ internal fun ChunkEntity.add(roomId: String,
this.displayIndex = currentDisplayIndex this.displayIndex = currentDisplayIndex
this.sendState = SendState.SYNCED this.sendState = SendState.SYNCED
} }
it.eventId = event.eventId ?: "" it.eventId = eventId
it.roomId = roomId 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 val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size
timelineEvents.add(position, eventEntity) timelineEvents.add(position, eventEntity)
@ -150,14 +170,14 @@ internal fun ChunkEntity.add(roomId: String,


internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {
return when (direction) { return when (direction) {
PaginationDirection.FORWARDS -> forwardsDisplayIndex PaginationDirection.FORWARDS -> forwardsDisplayIndex
PaginationDirection.BACKWARDS -> backwardsDisplayIndex PaginationDirection.BACKWARDS -> backwardsDisplayIndex
} ?: defaultValue } ?: defaultValue
} }


internal fun ChunkEntity.lastStateIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { internal fun ChunkEntity.lastStateIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {
return when (direction) { return when (direction) {
PaginationDirection.FORWARDS -> forwardsStateIndex PaginationDirection.FORWARDS -> forwardsStateIndex
PaginationDirection.BACKWARDS -> backwardsStateIndex PaginationDirection.BACKWARDS -> backwardsStateIndex
} ?: defaultValue } ?: defaultValue
} }

View File

@ -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<ReadReceipt> {
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())
}
}
}

}

View File

@ -26,7 +26,8 @@ import java.util.*
import javax.inject.Inject import javax.inject.Inject


internal class RoomSummaryMapper @Inject constructor( internal class RoomSummaryMapper @Inject constructor(
val cryptoService: CryptoService val cryptoService: CryptoService,
val timelineEventMapper: TimelineEventMapper
) { ) {


fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary { fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary {
@ -34,7 +35,9 @@ internal class RoomSummaryMapper @Inject constructor(
RoomTag(it.tagName, it.tagOrder) 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) { if (latestEvent?.root?.isEncrypted() == true && latestEvent.root.mxDecryptionResult == null) {
//TODO use a global event decryptor? attache to session and that listen to new sessionId? //TODO use a global event decryptor? attache to session and that listen to new sessionId?
//for now decrypt sync //for now decrypt sync

View File

@ -17,29 +17,30 @@
package im.vector.matrix.android.internal.database.mapper 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.events.model.Event

import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.database.model.TimelineEventEntity 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 { fun map(timelineEventEntity: TimelineEventEntity): TimelineEvent {


return TimelineEvent( return TimelineEvent(
root = timelineEventEntity.root?.asDomain() root = timelineEventEntity.root?.asDomain()
?: Event("", timelineEventEntity.eventId), ?: Event("", timelineEventEntity.eventId),
annotations = timelineEventEntity.annotations?.asDomain(), annotations = timelineEventEntity.annotations?.asDomain(),
localId = timelineEventEntity.localId, localId = timelineEventEntity.localId,
displayIndex = timelineEventEntity.root?.displayIndex ?: 0, displayIndex = timelineEventEntity.root?.displayIndex ?: 0,
senderName = timelineEventEntity.senderName, senderName = timelineEventEntity.senderName,
isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName, 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)
}




View File

@ -17,13 +17,18 @@
package im.vector.matrix.android.internal.database.model package im.vector.matrix.android.internal.database.model


import io.realm.RealmObject import io.realm.RealmObject
import io.realm.RealmResults
import io.realm.annotations.LinkingObjects
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey


internal open class ReadReceiptEntity(@PrimaryKey var primaryKey: String = "", internal open class ReadReceiptEntity(@PrimaryKey var primaryKey: String = "",
var userId: String = "", var eventId: String = "",
var eventId: String = "", var roomId: String = "",
var roomId: String = "", var userId: String = "",
var originServerTs: Double = 0.0 var originServerTs: Double = 0.0
) : RealmObject() { ) : RealmObject() {
companion object companion object

@LinkingObjects("readReceipts")
val summary: RealmResults<ReadReceiptsSummaryEntity>? = null
} }

View File

@ -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<ReadReceiptEntity> = RealmList()
) : RealmObject() {

companion object

}

View File

@ -22,26 +22,27 @@ import io.realm.annotations.RealmModule
* Realm module for Session * Realm module for Session
*/ */
@RealmModule(library = true, @RealmModule(library = true,
classes = [ classes = [
ChunkEntity::class, ChunkEntity::class,
EventEntity::class, EventEntity::class,
TimelineEventEntity::class, TimelineEventEntity::class,
FilterEntity::class, FilterEntity::class,
GroupEntity::class, GroupEntity::class,
GroupSummaryEntity::class, GroupSummaryEntity::class,
ReadReceiptEntity::class, ReadReceiptEntity::class,
RoomEntity::class, RoomEntity::class,
RoomSummaryEntity::class, RoomSummaryEntity::class,
RoomTagEntity::class, RoomTagEntity::class,
SyncEntity::class, SyncEntity::class,
UserEntity::class, UserEntity::class,
EventAnnotationsSummaryEntity::class, EventAnnotationsSummaryEntity::class,
ReactionAggregatedSummaryEntity::class, ReactionAggregatedSummaryEntity::class,
EditAggregatedSummaryEntity::class, EditAggregatedSummaryEntity::class,
PushRulesEntity::class, PushRulesEntity::class,
PushRuleEntity::class, PushRuleEntity::class,
PushConditionEntity::class, PushConditionEntity::class,
PusherEntity::class, PusherEntity::class,
PusherDataEntity::class PusherDataEntity::class,
]) ReadReceiptsSummaryEntity::class
])
internal class SessionRealmModule internal class SessionRealmModule

View File

@ -30,7 +30,8 @@ internal open class TimelineEventEntity(var localId: Long = 0,
var senderName: String? = null, var senderName: String? = null,
var isUniqueDisplayName: Boolean = false, var isUniqueDisplayName: Boolean = false,
var senderAvatar: String? = null, var senderAvatar: String? = null,
var senderMembershipEvent: EventEntity? = null var senderMembershipEvent: EventEntity? = null,
var readReceipts: ReadReceiptsSummaryEntity? = null
) : RealmObject() { ) : RealmObject() {


@LinkingObjects("timelineEvents") @LinkingObjects("timelineEvents")

View File

@ -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<ReadReceiptsSummaryEntity> {
return realm.where<ReadReceiptsSummaryEntity>()
.equalTo(ReadReceiptsSummaryEntityFields.EVENT_ID, eventId)
}

View File

@ -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.crypto.CryptoService
import im.vector.matrix.android.api.session.room.Room 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.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.DefaultMembershipService
import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask
import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask 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 monarchy: Monarchy,
private val eventFactory: LocalEchoEventFactory, private val eventFactory: LocalEchoEventFactory,
private val roomSummaryMapper: RoomSummaryMapper, private val roomSummaryMapper: RoomSummaryMapper,
private val timelineEventMapper: TimelineEventMapper,
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val loadRoomMembersTask: LoadRoomMembersTask, private val loadRoomMembersTask: LoadRoomMembersTask,
private val inviteTask: InviteTask, private val inviteTask: InviteTask,
@ -61,13 +63,13 @@ internal class RoomFactory @Inject constructor(private val context: Context,
private val leaveRoomTask: LeaveRoomTask) { private val leaveRoomTask: LeaveRoomTask) {


fun create(roomId: String): Room { 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 sendService = DefaultSendService(context, credentials, roomId, eventFactory, cryptoService, monarchy)
val stateService = DefaultStateService(roomId, monarchy.realmConfiguration, taskExecutor, sendStateTask) val stateService = DefaultStateService(roomId, monarchy.realmConfiguration, taskExecutor, sendStateTask)
val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask) 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, credentials)
val relationService = DefaultRelationService(context, val relationService = DefaultRelationService(context,
credentials, roomId, eventFactory, cryptoService, findReactionEventForUndoTask, fetchEditHistoryTask, monarchy, taskExecutor) credentials, roomId, eventFactory, cryptoService, findReactionEventForUndoTask, fetchEditHistoryTask, monarchy, taskExecutor)


return DefaultRoom( return DefaultRoom(
roomId, roomId,

View File

@ -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.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.util.CancelableBag 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.mapper.asDomain
import im.vector.matrix.android.internal.database.model.* import im.vector.matrix.android.internal.database.model.*
import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntity
@ -53,7 +54,8 @@ internal class DefaultTimeline(
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val contextOfEventTask: GetContextOfEventTask, private val contextOfEventTask: GetContextOfEventTask,
private val paginationTask: PaginationTask, private val paginationTask: PaginationTask,
cryptoService: CryptoService, private val cryptoService: CryptoService,
private val timelineEventMapper: TimelineEventMapper,
private val allowedTypes: List<String>? private val allowedTypes: List<String>?
) : Timeline { ) : Timeline {


@ -132,7 +134,7 @@ internal class DefaultTimeline(
builtEventsIdMap[eventId]?.let { builtIndex -> builtEventsIdMap[eventId]?.let { builtIndex ->
//Update the relation of existing event //Update the relation of existing event
builtEvents[builtIndex]?.let { te -> builtEvents[builtIndex]?.let { te ->
builtEvents[builtIndex] = eventEntity.asDomain() builtEvents[builtIndex] = timelineEventMapper.map(eventEntity)
hasChanged = true hasChanged = true
} }
} }
@ -331,7 +333,7 @@ internal class DefaultTimeline(
roomEntity?.sendingTimelineEvents roomEntity?.sendingTimelineEvents
?.filter { allowedTypes?.contains(it.root?.type) ?: false } ?.filter { allowedTypes?.contains(it.root?.type) ?: false }
?.forEach { ?.forEach {
sendingEvents.add(it.asDomain()) sendingEvents.add(timelineEventMapper.map(it))
} }
} }
return sendingEvents return sendingEvents
@ -463,7 +465,7 @@ internal class DefaultTimeline(
nextDisplayIndex = offsetIndex + 1 nextDisplayIndex = offsetIndex + 1
} }
offsetResults.forEach { eventEntity -> offsetResults.forEach { eventEntity ->
val timelineEvent = eventEntity.asDomain() val timelineEvent = timelineEventMapper.map(eventEntity)


if (timelineEvent.isEncrypted() if (timelineEvent.isEncrypted()
&& timelineEvent.root.mxDecryptionResult == null) { && timelineEvent.root.mxDecryptionResult == null) {

View File

@ -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.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.TimelineService 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.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.mapper.asDomain
import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.query.where 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 taskExecutor: TaskExecutor,
private val contextOfEventTask: GetContextOfEventTask, private val contextOfEventTask: GetContextOfEventTask,
private val cryptoService: CryptoService, private val cryptoService: CryptoService,
private val paginationTask: PaginationTask private val paginationTask: PaginationTask,
private val timelineEventMapper: TimelineEventMapper
) : TimelineService { ) : TimelineService {


override fun createTimeline(eventId: String?, allowedTypes: List<String>?): Timeline { override fun createTimeline(eventId: String?, allowedTypes: List<String>?): Timeline {
@ -47,7 +49,9 @@ internal class DefaultTimelineService @Inject constructor(private val roomId: St
contextOfEventTask, contextOfEventTask,
paginationTask, paginationTask,
cryptoService, cryptoService,
allowedTypes) timelineEventMapper,
allowedTypes
)
} }


override fun getTimeLineEvent(eventId: String): TimelineEvent? { override fun getTimeLineEvent(eventId: String): TimelineEvent? {
@ -55,7 +59,7 @@ internal class DefaultTimelineService @Inject constructor(private val roomId: St
.fetchCopyMap({ .fetchCopyMap({
TimelineEventEntity.where(it, eventId = eventId).findFirst() TimelineEventEntity.where(it, eventId = eventId).findFirst()
}, { entity, realm -> }, { 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) { val liveData = RealmLiveData(monarchy.realmConfiguration) {
TimelineEventEntity.where(it, eventId = eventId) TimelineEventEntity.where(it, eventId = eventId)
} }
return Transformations.map(liveData) { return Transformations.map(liveData) { events ->
it.firstOrNull()?.asDomain() events.firstOrNull()?.let { timelineEventMapper.map(it) }
} }
} }



View File

@ -17,6 +17,8 @@
package im.vector.matrix.android.internal.session.sync 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.ReadReceiptEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity
import im.vector.matrix.android.internal.database.query.where
import io.realm.Realm import io.realm.Realm
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -36,27 +38,35 @@ internal class ReadReceiptHandler @Inject constructor() {
return return
} }
try { try {
val readReceipts = mapContentToReadReceiptEntities(roomId, content) handleReadReceiptContent(realm, roomId, content)
realm.insertOrUpdate(readReceipts)
} catch (exception: Exception) { } catch (exception: Exception) {
Timber.e("Fail to handle read receipt for room $roomId") Timber.e("Fail to handle read receipt for room $roomId")
} }
} }


private fun mapContentToReadReceiptEntities(roomId: String, content: ReadReceiptContent): List<ReadReceiptEntity> { private fun handleReadReceiptContent(realm: Realm, roomId: String, content: ReadReceiptContent) {
return content for ((eventId, receiptDict) in content) {
.flatMap { (eventId, receiptDict) -> val userIdsDict = receiptDict["m.read"] ?: continue
receiptDict val readReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst()
.filterKeys { it == "m.read" } ?: realm.createObject(ReadReceiptsSummaryEntity::class.java, eventId)
.flatMap { (_, userIdsDict) ->
userIdsDict.map { (userId, paramsDict) -> for ((userId, paramsDict) in userIdsDict) {
val ts = paramsDict.filterKeys { it == "ts" } val ts = paramsDict["ts"] ?: 0.0
.values val primaryKey = "${roomId}_$userId"
.firstOrNull() ?: 0.0 val receiptEntity = ReadReceiptEntity.where(realm, roomId, userId).findFirst()
val primaryKey = roomId + userId ?: realm.createObject(ReadReceiptEntity::class.java, primaryKey)
ReadReceiptEntity(primaryKey, userId, eventId, roomId, ts)
} 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)
}
}
} }
} }

View File

@ -117,7 +117,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
Timber.v("Handle join sync for room $roomId") Timber.v("Handle join sync for room $roomId")


val roomEntity = RoomEntity.where(realm, roomId).findFirst() val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId) ?: realm.createObject(roomId)


if (roomEntity.membership == Membership.INVITE) { if (roomEntity.membership == Membership.INVITE) {
roomEntity.chunks.deleteAllFromRealm() roomEntity.chunks.deleteAllFromRealm()
@ -127,7 +127,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
// State event // State event
if (roomSync.state != null && roomSync.state.events.isNotEmpty()) { if (roomSync.state != null && roomSync.state.events.isNotEmpty()) {
val minStateIndex = roomEntity.untimelinedStateEvents.where().min(EventEntityFields.STATE_INDEX)?.toInt() val minStateIndex = roomEntity.untimelinedStateEvents.where().min(EventEntityFields.STATE_INDEX)?.toInt()
?: Int.MIN_VALUE ?: Int.MIN_VALUE
val untimelinedStateIndex = minStateIndex + 1 val untimelinedStateIndex = minStateIndex + 1
roomSync.state.events.forEach { event -> roomSync.state.events.forEach { event ->
roomEntity.addStateEvent(event, filterDuplicates = true, stateIndex = untimelinedStateIndex) roomEntity.addStateEvent(event, filterDuplicates = true, stateIndex = untimelinedStateIndex)
@ -167,7 +167,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
InvitedRoomSync): RoomEntity { InvitedRoomSync): RoomEntity {
Timber.v("Handle invited sync for room $roomId") Timber.v("Handle invited sync for room $roomId")
val roomEntity = RoomEntity.where(realm, roomId).findFirst() val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId) ?: realm.createObject(roomId)
roomEntity.membership = Membership.INVITE roomEntity.membership = Membership.INVITE
if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) { if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) {
val chunkEntity = handleTimelineEvents(realm, roomEntity, roomSync.inviteState.events) val chunkEntity = handleTimelineEvents(realm, roomEntity, roomSync.inviteState.events)
@ -181,7 +181,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
roomId: String, roomId: String,
roomSync: RoomSync): RoomEntity { roomSync: RoomSync): RoomEntity {
val roomEntity = RoomEntity.where(realm, roomId).findFirst() val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId) ?: realm.createObject(roomId)


roomEntity.membership = Membership.LEAVE roomEntity.membership = Membership.LEAVE
roomEntity.chunks.deleteAllFromRealm() roomEntity.chunks.deleteAllFromRealm()
@ -233,17 +233,20 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
} }




@Suppress("UNCHECKED_CAST")
private fun handleEphemeral(realm: Realm, private fun handleEphemeral(realm: Realm,
roomId: String, roomId: String,
ephemeral: RoomSyncEphemeral) { ephemeral: RoomSyncEphemeral) {
ephemeral.events for (event in ephemeral.events) {
.filter { it.getClearType() == EventType.RECEIPT } if (event.type != EventType.RECEIPT) continue
.map { it.content.toModel<ReadReceiptContent>() } val readReceiptContent = event.content as? ReadReceiptContent ?: continue
.forEach { readReceiptHandler.handle(realm, roomId, it) } readReceiptHandler.handle(realm, roomId, readReceiptContent)
}
} }


private fun handleRoomAccountDataEvents(realm: Realm, roomId: String, accountData: RoomSyncAccountData) { private fun handleRoomAccountDataEvents(realm: Realm, roomId: String, accountData: RoomSyncAccountData) {
accountData.events accountData.events
.asSequence()
.filter { it.getClearType() == EventType.TAG } .filter { it.getClearType() == EventType.TAG }
.map { it.content.toModel<RoomTagContent>() } .map { it.content.toModel<RoomTagContent>() }
.forEach { roomTagHandler.handle(realm, roomId, it) } .forEach { roomTagHandler.handle(realm, roomId, it) }

View File

@ -161,7 +161,6 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector {


override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()

unBinder?.unbind() unBinder?.unbind()
unBinder = null unBinder = null



View File

@ -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.permalinks.MatrixPermalinkSpan
import im.vector.matrix.android.api.session.events.model.RelationType 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.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent
import im.vector.matrix.android.api.session.room.send.SendState 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.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt 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.ContentUploadStateTrackerBinder
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider 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.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.home.room.detail.timeline.util.MessageInformationDataFactory
import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.html.EventHtmlRenderer
import im.vector.riotx.features.media.ImageContentRenderer import im.vector.riotx.features.media.ImageContentRenderer
@ -84,11 +103,11 @@ class MessageItemFactory @Inject constructor(


val messageContent: MessageContent = val messageContent: MessageContent =
event.getLastMessageContent() event.getLastMessageContent()
?: //Malformed content, we should echo something on screen ?: //Malformed content, we should echo something on screen
return DefaultItem_().text(stringProvider.getString(R.string.malformed_message)) return DefaultItem_().text(stringProvider.getString(R.string.malformed_message))


if (messageContent.relatesTo?.type == RelationType.REPLACE if (messageContent.relatesTo?.type == RelationType.REPLACE
|| event.isEncrypted() && event.root.content.toModel<EncryptedEventContent>()?.relatesTo?.type == RelationType.REPLACE || event.isEncrypted() && event.root.content.toModel<EncryptedEventContent>()?.relatesTo?.type == RelationType.REPLACE
) { ) {
// ignore replace event, the targeted id is already edited // ignore replace event, the targeted id is already edited
if (userPreferencesProvider.shouldShowHiddenEvents()) { if (userPreferencesProvider.shouldShowHiddenEvents()) {
@ -116,15 +135,13 @@ class MessageItemFactory @Inject constructor(
// val ev = all.toModel<Event>() // val ev = all.toModel<Event>()
return when (messageContent) { return when (messageContent) {
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, is MessageEmoteContent -> buildEmoteMessageItem(messageContent,
informationData, informationData,
highlight, highlight,
callback) callback)
is MessageTextContent -> buildTextMessageItem(event.root.sendState, is MessageTextContent -> buildTextMessageItem(messageContent,
messageContent, informationData,
informationData, highlight,
highlight, callback)
callback
)
is MessageImageContent -> buildImageMessageItem(messageContent, informationData, highlight, callback) is MessageImageContent -> buildImageMessageItem(messageContent, informationData, highlight, callback)
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback) is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback)
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback) is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback)
@ -158,7 +175,7 @@ class MessageItemFactory @Inject constructor(
})) }))
.longClickListener { view -> .longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false ?: false
} }
} }


@ -182,7 +199,7 @@ class MessageItemFactory @Inject constructor(
})) }))
.longClickListener { view -> .longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false ?: false
} }
.clickListener( .clickListener(
DebouncedClickListener(View.OnClickListener { _ -> DebouncedClickListener(View.OnClickListener { _ ->
@ -190,7 +207,7 @@ class MessageItemFactory @Inject constructor(
})) }))
.longClickListener { view -> .longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false ?: false
} }
} }


@ -240,7 +257,7 @@ class MessageItemFactory @Inject constructor(
})) }))
.longClickListener { view -> .longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false ?: false
} }
} }


@ -253,7 +270,7 @@ class MessageItemFactory @Inject constructor(
val thumbnailData = ImageContentRenderer.Data( val thumbnailData = ImageContentRenderer.Data(
filename = messageContent.body, filename = messageContent.body,
url = messageContent.videoInfo?.thumbnailFile?.url url = messageContent.videoInfo?.thumbnailFile?.url
?: messageContent.videoInfo?.thumbnailUrl, ?: messageContent.videoInfo?.thumbnailUrl,
elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(), elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(),
height = messageContent.videoInfo?.height, height = messageContent.videoInfo?.height,
maxHeight = maxHeight, maxHeight = maxHeight,
@ -288,12 +305,11 @@ class MessageItemFactory @Inject constructor(
.clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) } .clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) }
.longClickListener { view -> .longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false ?: false
} }
} }


private fun buildTextMessageItem(sendState: SendState, private fun buildTextMessageItem(messageContent: MessageTextContent,
messageContent: MessageTextContent,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?): MessageTextItem? { callback: TimelineEventController.Callback?): MessageTextItem? {
@ -328,7 +344,7 @@ class MessageItemFactory @Inject constructor(
})) }))
.longClickListener { view -> .longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false ?: false
} }
} }


@ -358,9 +374,9 @@ class MessageItemFactory @Inject constructor(
//nop //nop
} }
}, },
editStart, editStart,
editEnd, editEnd,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE) Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
return spannable return spannable
} }


@ -397,7 +413,7 @@ class MessageItemFactory @Inject constructor(
})) }))
.longClickListener { view -> .longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false ?: false
} }
} }


@ -433,7 +449,7 @@ class MessageItemFactory @Inject constructor(
})) }))
.longClickListener { view -> .longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false ?: false
} }
} }


@ -452,7 +468,7 @@ class MessageItemFactory @Inject constructor(
})) }))
.longClickListener { view -> .longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, null, view) return@longClickListener callback?.onEventLongClicked(informationData, null, view)
?: false ?: false
} }
} }



View File

@ -16,6 +16,7 @@


package im.vector.riotx.features.home.room.detail.timeline.item package im.vector.riotx.features.home.room.detail.timeline.item


import android.annotation.SuppressLint
import android.graphics.Typeface import android.graphics.Typeface
import android.os.Build import android.os.Build
import android.view.View 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.reactions.widget.ReactionButton
import im.vector.riotx.features.ui.getMessageTextColor import im.vector.riotx.features.ui.getMessageTextColor


private const val MAX_RECEIPT_DISPLAYED = 5


abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() { abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {


@ -123,6 +125,29 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
holder.memberNameView.setOnLongClickListener(null) 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()) { if (!shouldShowReactionAtBottom() || informationData.orderedReactionList.isNullOrEmpty()) {
holder.reactionWrapper?.isVisible = false holder.reactionWrapper?.isVisible = false
} else { } else {
@ -173,6 +198,16 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView) val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
val memberNameView by bind<TextView>(R.id.messageMemberNameView) val memberNameView by bind<TextView>(R.id.messageMemberNameView)
val timeView by bind<TextView>(R.id.messageTimeView) val timeView by bind<TextView>(R.id.messageTimeView)
val readReceiptsView by bind<ViewGroup>(R.id.readReceiptsView)
val receiptAvatar1 by bind<ImageView>(R.id.message_avatar_receipt_1)
val receiptAvatar2 by bind<ImageView>(R.id.message_avatar_receipt_2)
val receiptAvatar3 by bind<ImageView>(R.id.message_avatar_receipt_3)
val receiptAvatar4 by bind<ImageView>(R.id.message_avatar_receipt_4)
val receiptAvatar5 by bind<ImageView>(R.id.message_avatar_receipt_5)
val receiptMoreView by bind<TextView>(R.id.message_more_than_expected)
val receiptAvatars: List<ImageView> by lazy {
listOf(receiptAvatar1, receiptAvatar2, receiptAvatar3, receiptAvatar4, receiptAvatar5)
}


var reactionWrapper: ViewGroup? = null var reactionWrapper: ViewGroup? = null
var reactionFlowHelper: Flow? = null var reactionFlowHelper: Flow? = null

View File

@ -17,6 +17,7 @@
package im.vector.riotx.features.home.room.detail.timeline.item package im.vector.riotx.features.home.room.detail.timeline.item


import android.os.Parcelable 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 im.vector.matrix.android.api.session.room.send.SendState
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize


@ -32,7 +33,8 @@ data class MessageInformationData(
/*List of reactions (emoji,count,isSelected)*/ /*List of reactions (emoji,count,isSelected)*/
val orderedReactionList: List<ReactionInfoData>? = null, val orderedReactionList: List<ReactionInfoData>? = null,
val hasBeenEdited: Boolean = false, val hasBeenEdited: Boolean = false,
val hasPendingEdits: Boolean = false val hasPendingEdits: Boolean = false,
val readReceipts: List<ReadReceiptData> = emptyList()
) : Parcelable ) : Parcelable




@ -43,3 +45,11 @@ data class ReactionInfoData(
val addedByMe: Boolean, val addedByMe: Boolean,
val synced: Boolean val synced: Boolean
) : Parcelable ) : Parcelable

@Parcelize
data class ReadReceiptData(
val userId: String,
val avatarUrl: String?,
val displayName: String?,
val timestamp: Long
) : Parcelable

View File

@ -16,6 +16,7 @@


package im.vector.riotx.features.home.room.detail.timeline.util 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.events.model.EventType
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited 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.helper.TimelineDateFormatter
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData 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.ReactionInfoData
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
import me.gujun.android.span.span import me.gujun.android.span.span
import javax.inject.Inject 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 * 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) { private val colorProvider: ColorProvider) {


fun create(event: TimelineEvent, nextEvent: TimelineEvent?): MessageInformationData { fun create(event: TimelineEvent, nextEvent: TimelineEvent?): MessageInformationData {
@ -43,21 +46,21 @@ class MessageInformationDataFactory @Inject constructor(private val timelineDate
val nextDate = nextEvent?.root?.localDateTime() val nextDate = nextEvent?.root?.localDateTime()
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate() val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
val isNextMessageReceivedMoreThanOneHourAgo = nextDate?.isBefore(date.minusMinutes(60)) val isNextMessageReceivedMoreThanOneHourAgo = nextDate?.isBefore(date.minusMinutes(60))
?: false ?: false


val showInformation = val showInformation =
addDaySeparator addDaySeparator
|| event.senderAvatar != nextEvent?.senderAvatar || event.senderAvatar != nextEvent?.senderAvatar
|| event.getDisambiguatedDisplayName() != nextEvent?.getDisambiguatedDisplayName() || 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 || isNextMessageReceivedMoreThanOneHourAgo


val time = timelineDateFormatter.formatMessageHour(date) val time = timelineDateFormatter.formatMessageHour(date)
val avatarUrl = event.senderAvatar val avatarUrl = event.senderAvatar
val memberName = event.getDisambiguatedDisplayName() val memberName = event.getDisambiguatedDisplayName()
val formattedMemberName = span(memberName) { val formattedMemberName = span(memberName) {
textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId
?: "")) ?: ""))
} }


return MessageInformationData( return MessageInformationData(
@ -74,7 +77,14 @@ class MessageInformationDataFactory @Inject constructor(private val timelineDate
ReactionInfoData(it.key, it.count, it.addedByMe, it.localEchoEvents.isEmpty()) ReactionInfoData(it.key, it.count, it.addedByMe, it.localEchoEvents.isEmpty())
}, },
hasBeenEdited = event.hasBeenEdited(), 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)
}
) )
} }
} }

View File

@ -114,7 +114,7 @@
android:inflatedId="@+id/messageBottomInfo" android:inflatedId="@+id/messageBottomInfo"
android:layout="@layout/item_timeline_event_bottom_reactions_stub" android:layout="@layout/item_timeline_event_bottom_reactions_stub"
android:visibility="gone" android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toTopOf="@+id/readReceiptsView"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/messageStartGuideline" app:layout_constraintStart_toEndOf="@id/messageStartGuideline"
app:layout_constraintVertical_chainStyle="packed" app:layout_constraintVertical_chainStyle="packed"
@ -123,4 +123,65 @@
</ViewStub> </ViewStub>




<LinearLayout
android:id="@+id/readReceiptsView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent">

<TextView
android:id="@+id/message_more_than_expected"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginEnd="8dp"
android:gravity="center"
android:textSize="12sp"
tools:text="999+" />

<ImageView
android:id="@+id/message_avatar_receipt_5"
android:layout_width="16dp"
android:layout_height="16dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
tools:src="@tools:sample/avatars" />

<ImageView
android:id="@+id/message_avatar_receipt_4"
android:layout_width="16dp"
android:layout_height="16dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
tools:src="@tools:sample/avatars" />

<ImageView
android:id="@+id/message_avatar_receipt_3"
android:layout_width="16dp"
android:layout_height="16dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
tools:src="@tools:sample/avatars" />

<ImageView
android:id="@+id/message_avatar_receipt_2"
android:layout_width="16dp"
android:layout_height="16dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
tools:src="@tools:sample/avatars" />

<ImageView
android:id="@+id/message_avatar_receipt_1"
android:layout_width="16dp"
android:layout_height="16dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
tools:src="@tools:sample/avatars" />

</LinearLayout>


</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/readReceiptsView"
android:layout_width="match_parent"
android:layout_height="wrap_content">



</RelativeLayout>