From 41e168a5193918b27716d862c27bb35d119192bd Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 9 Oct 2020 00:30:20 +0200 Subject: [PATCH] add UI echo of reactions --- .../room/model/relation/ReactionInfo.kt | 2 +- .../session/room/timeline/DefaultTimeline.kt | 206 +++++++++++++++--- 2 files changed, 171 insertions(+), 37 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionInfo.kt index 5b5c9e6886..733d6c37e8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionInfo.kt @@ -23,7 +23,7 @@ import com.squareup.moshi.JsonClass data class ReactionInfo( @Json(name = "rel_type") override val type: String?, @Json(name = "event_id") override val eventId: String, - val key: String, + @Json(name = "key") val key: String, // always null for reaction @Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null, @Json(name = "option") override val option: Int? = null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index 552a7ccbd6..10af6cb220 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -32,8 +32,11 @@ import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary +import org.matrix.android.sdk.api.session.room.model.ReactionAggregatedSummary import org.matrix.android.sdk.api.session.room.model.ReadReceipt import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent @@ -103,8 +106,9 @@ internal class DefaultTimeline( private var prevDisplayIndex: Int? = null private var nextDisplayIndex: Int? = null - private val inMemorySendingEvents = Collections.synchronizedList(ArrayList()) - private val inMemorySendingStates = Collections.synchronizedMap(HashMap()) + + private val uiEchoManager = UIEchoManager() + private val builtEvents = Collections.synchronizedList(ArrayList()) private val builtEventsIdMap = Collections.synchronizedMap(HashMap()) private val backwardsState = AtomicReference(State()) @@ -165,13 +169,7 @@ internal class DefaultTimeline( sendingEvents = roomEntity.sendingTimelineEvents.where().filterEventsWithSettings().findAll() sendingEvents.addChangeListener { events -> - // Remove in memory as soon as they are known by database - events.forEach { te -> - inMemorySendingEvents.removeAll { te.eventId == it.eventId } - } - inMemorySendingStates.keys.removeAll { key -> - events.find { it.eventId == key } == null - } + uiEchoManager.sentEventsUpdated(events) postSnapshot() } nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll() @@ -323,28 +321,15 @@ internal class DefaultTimeline( @Subscribe(threadMode = ThreadMode.MAIN) fun onLocalEchoCreated(onLocalEchoCreated: OnLocalEchoCreated) { - if (isLive && onLocalEchoCreated.roomId == roomId) { - // do not add events that would have been filtered - if (listOf(onLocalEchoCreated.timelineEvent).filterEventsWithSettings().isNotEmpty()) { - listeners.forEach { - it.onNewTimelineEvents(listOf(onLocalEchoCreated.timelineEvent.eventId)) - } - Timber.v("On local echo created: ${onLocalEchoCreated.timelineEvent.eventId}") - inMemorySendingEvents.add(0, onLocalEchoCreated.timelineEvent) - postSnapshot() - } + if (uiEchoManager.onLocalEchoCreated(onLocalEchoCreated)) { + postSnapshot() } } @Subscribe(threadMode = ThreadMode.MAIN) fun onLocalEchoUpdated(onLocalEchoUpdated: OnLocalEchoUpdated) { - if (isLive && onLocalEchoUpdated.roomId == roomId) { - val existingState = inMemorySendingStates[onLocalEchoUpdated.eventId] - inMemorySendingStates[onLocalEchoUpdated.eventId] = onLocalEchoUpdated.sendState - if (existingState != onLocalEchoUpdated.sendState) { - // Timber.v("## SendEvent: onLocalEchoUpdated $onLocalEchoUpdated in mem size = ${inMemorySendingStates.size}") - postSnapshot() - } + if (uiEchoManager.onLocalEchoUpdated(onLocalEchoUpdated)) { + postSnapshot() } } @@ -424,14 +409,11 @@ internal class DefaultTimeline( private fun buildSendingEvents(): List { val builtSendingEvents = ArrayList() if (hasReachedEnd(Timeline.Direction.FORWARDS) && !hasMoreInCache(Timeline.Direction.FORWARDS)) { - builtSendingEvents.addAll(inMemorySendingEvents.filterEventsWithSettings()) + builtSendingEvents.addAll(uiEchoManager.getInMemorySendingEvents().filterEventsWithSettings()) sendingEvents.forEach { timelineEventEntity -> if (builtSendingEvents.find { it.eventId == timelineEventEntity.eventId } == null) { val element = timelineEventMapper.map(timelineEventEntity) - inMemorySendingStates[element.eventId]?.let { - // Timber.v("## ${System.currentTimeMillis()} Send event refresh echo with live state ${it} from state ${element.root.sendState}") - element.root.sendState = element.root.sendState.takeIf { it == SendState.SENT } ?: it - } + uiEchoManager.updateSentStateWithUiEcho(element) builtSendingEvents.add(element) } } @@ -644,10 +626,7 @@ internal class DefaultTimeline( val timelineEvent = buildTimelineEvent(eventEntity) val transactionId = timelineEvent.root.unsignedData?.transactionId - val sendingEvent = inMemorySendingEvents.find { - it.eventId == transactionId - } - inMemorySendingEvents.remove(sendingEvent) + uiEchoManager.onSyncedEvent(transactionId) if (timelineEvent.isEncrypted() && timelineEvent.root.mxDecryptionResult == null) { @@ -671,7 +650,10 @@ internal class DefaultTimeline( timelineEventEntity = eventEntity, buildReadReceipts = settings.buildReadReceipts, correctedReadReceipts = hiddenReadReceipts.correctedReadReceipts(eventEntity.eventId) - ) + ).let { + // eventually enhance with ui echo? + (uiEchoManager.decorateEventWithReactionUiEcho(it) ?: it) + } /** * This has to be called on TimelineThread as it accesses realm live results @@ -819,4 +801,156 @@ internal class DefaultTimeline( val isPaginating: Boolean = false, val requestedPaginationCount: Int = 0 ) + + private data class ReactionUiEchoData( + val localEchoId: String, + val reactedOnEventId: String, + val reaction: String + ) + + inner class UIEchoManager { + + private val inMemorySendingEvents = Collections.synchronizedList(ArrayList()) + + fun getInMemorySendingEvents(): List { + return inMemorySendingEvents + } + + /** + * Due to lag of DB updates, we keep some UI echo of some properties to update timeline faster + */ + private val inMemorySendingStates = Collections.synchronizedMap(HashMap()) + + private val inMemoryReactions = Collections.synchronizedMap>(HashMap()) + + fun sentEventsUpdated(events: RealmResults) { + // Remove in memory as soon as they are known by database + events.forEach { te -> + inMemorySendingEvents.removeAll { te.eventId == it.eventId } + } + inMemorySendingStates.keys.removeAll { key -> + events.find { it.eventId == key } == null + } + inMemoryReactions.keys.removeAll { key -> + events.find { it.eventId == key } == null + } + } + + fun onLocalEchoUpdated(onLocalEchoUpdated: OnLocalEchoUpdated): Boolean { + if (isLive && onLocalEchoUpdated.roomId == roomId) { + val existingState = inMemorySendingStates[onLocalEchoUpdated.eventId] + inMemorySendingStates[onLocalEchoUpdated.eventId] = onLocalEchoUpdated.sendState + if (existingState != onLocalEchoUpdated.sendState) { + return true + } + } + return false + } + + // return true if should update + fun onLocalEchoCreated(onLocalEchoCreated: OnLocalEchoCreated): Boolean { + var postSnapshot = false + if (isLive && onLocalEchoCreated.roomId == roomId) { + + // Manage some ui echos (do it before filter because actual event could be filtered out) + when (onLocalEchoCreated.timelineEvent.root.getClearType()) { + EventType.REDACTION -> { + + } + EventType.REACTION -> { + val content = onLocalEchoCreated.timelineEvent.root.content?.toModel() + if (RelationType.ANNOTATION == content?.relatesTo?.type) { + val reaction = content.relatesTo.key + val relatedEventID = content.relatesTo.eventId + (inMemoryReactions.getOrPut(relatedEventID) { mutableListOf() }).add( + ReactionUiEchoData( + localEchoId = onLocalEchoCreated.timelineEvent.eventId, + reactedOnEventId = relatedEventID, + reaction = reaction + ) + ) + postSnapshot = rebuildEvent(relatedEventID) { + decorateEventWithReactionUiEcho(it) + } || postSnapshot + } + } + } + + // do not add events that would have been filtered + if (listOf(onLocalEchoCreated.timelineEvent).filterEventsWithSettings().isNotEmpty()) { + listeners.forEach { + it.onNewTimelineEvents(listOf(onLocalEchoCreated.timelineEvent.eventId)) + } + Timber.v("On local echo created: ${onLocalEchoCreated.timelineEvent.eventId}") + inMemorySendingEvents.add(0, onLocalEchoCreated.timelineEvent) + postSnapshot = true + } + } + return postSnapshot + } + + fun decorateEventWithReactionUiEcho(timelineEvent: TimelineEvent): TimelineEvent? { + val relatedEventID = timelineEvent.eventId + val contents = inMemoryReactions[relatedEventID] ?: return null + + var existingAnnotationSummary = timelineEvent.annotations ?: EventAnnotationsSummary( + relatedEventID + ) + val updateReactions = existingAnnotationSummary.reactionsSummary.toMutableList() + contents.forEach { uiEchoReaction -> + val existing = updateReactions.firstOrNull { it.key == uiEchoReaction.reaction } + if (existing == null) { + // just add the new key + ReactionAggregatedSummary( + key = uiEchoReaction.reaction, + count = 1, + addedByMe = true, + firstTimestamp = System.currentTimeMillis(), + sourceEvents = emptyList(), + localEchoEvents = listOf(uiEchoReaction.localEchoId) + ).let { updateReactions.add(it) } + } else { + // update Existing Key + if (!existing.localEchoEvents.contains(uiEchoReaction.localEchoId)) { + updateReactions.remove(existing) + // only update if echo is not yet there + ReactionAggregatedSummary( + key = existing.key, + count = existing.count + 1, + addedByMe = true, + firstTimestamp = existing.firstTimestamp, + sourceEvents = existing.sourceEvents, + localEchoEvents = existing.localEchoEvents + uiEchoReaction.localEchoId + + ).let { updateReactions.add(it) } + } + } + } + + existingAnnotationSummary = existingAnnotationSummary.copy( + reactionsSummary = updateReactions + ) + return timelineEvent.copy( + annotations = existingAnnotationSummary + ) + } + + fun updateSentStateWithUiEcho(element: TimelineEvent) { + inMemorySendingStates[element.eventId]?.let { + // Timber.v("## ${System.currentTimeMillis()} Send event refresh echo with live state ${it} from state ${element.root.sendState}") + element.root.sendState = element.root.sendState.takeIf { it == SendState.SENT } ?: it + } + } + + fun onSyncedEvent(transactionId: String?) { + val sendingEvent = inMemorySendingEvents.find { + it.eventId == transactionId + } + inMemorySendingEvents.remove(sendingEvent) + // Is it too early to clear it? will be done when removed from sending anyway? + inMemoryReactions.forEach { (_, u) -> + u.filterNot { it.localEchoId == transactionId } + } + } + } }