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 ca272a3520..f3f7e5d162 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 @@ -28,13 +28,7 @@ import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.extensions.orFalse 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 @@ -46,7 +40,6 @@ import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.RoomEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields -import org.matrix.android.sdk.internal.database.query.filterEvents import org.matrix.android.sdk.internal.database.query.findAllInRoomWithSendStates import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.whereRoomId @@ -83,7 +76,8 @@ internal class DefaultTimeline( private val loadRoomMembersTask: LoadRoomMembersTask ) : Timeline, TimelineHiddenReadReceipts.Delegate, - TimelineInput.Listener { + TimelineInput.Listener, + UIEchoManager.Listener { companion object { val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD") @@ -104,12 +98,12 @@ internal class DefaultTimeline( private var prevDisplayIndex: Int? = null private var nextDisplayIndex: Int? = null - private val uiEchoManager = UIEchoManager() + private val uiEchoManager = UIEchoManager(settings, this) private val builtEvents = Collections.synchronizedList(ArrayList()) private val builtEventsIdMap = Collections.synchronizedMap(HashMap()) - private val backwardsState = AtomicReference(State()) - private val forwardsState = AtomicReference(State()) + private val backwardsState = AtomicReference(TimelineState()) + private val forwardsState = AtomicReference(TimelineState()) override val timelineID = UUID.randomUUID().toString() @@ -174,7 +168,7 @@ internal class DefaultTimeline( nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll() filteredEvents = nonFilteredEvents.where() - .filterEventsWithSettings() + .filterEventsWithSettings(settings) .findAll() nonFilteredEvents.addChangeListener(eventsChangeListener) handleInitialLoad() @@ -260,7 +254,9 @@ internal class DefaultTimeline( .equalTo(TimelineEventEntityFields.EVENT_ID, eventId) .findFirst() - val filteredEvents = nonFilteredEvents.where().filterEventsWithSettings().findAll() + val filteredEvents = nonFilteredEvents.where() + .filterEventsWithSettings(settings) + .findAll() val isEventInDb = nonFilteredEvent != null val isHidden = isEventInDb && filteredEvents.where() @@ -326,20 +322,29 @@ internal class DefaultTimeline( } override fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent) { - if (uiEchoManager.onLocalEchoCreated(roomId, timelineEvent)) { + if (roomId != this.roomId || !isLive) return + + val postSnapShot = uiEchoManager.onLocalEchoCreated(timelineEvent) + + if (listOf(timelineEvent).filterEventsWithSettings(settings).isNotEmpty()) { + listeners.forEach { + it.onNewTimelineEvents(listOf(timelineEvent.eventId)) + } + } + + if (postSnapShot) { postSnapshot() } } override fun onLocalEchoUpdated(roomId: String, eventId: String, sendState: SendState) { - if (uiEchoManager.onLocalEchoUpdated(roomId, eventId, sendState)) { + if (roomId != this.roomId || !isLive) return + if (uiEchoManager.onLocalEchoUpdated(eventId, sendState)) { postSnapshot() } } -// Private methods ***************************************************************************** - - private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent?): Boolean { + override fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent?): Boolean { return tryOrNull { builtEventsIdMap[eventId]?.let { builtIndex -> // Update the relation of existing event @@ -359,6 +364,8 @@ internal class DefaultTimeline( } ?: false } +// Private methods ***************************************************************************** + private fun hasMoreInCache(direction: Timeline.Direction) = getState(direction).hasMoreInCache private fun hasReachedEnd(direction: Timeline.Direction) = getState(direction).hasReachedEnd @@ -413,11 +420,15 @@ internal class DefaultTimeline( private fun buildSendingEvents(): List { val builtSendingEvents = ArrayList() if (hasReachedEnd(Timeline.Direction.FORWARDS) && !hasMoreInCache(Timeline.Direction.FORWARDS)) { - builtSendingEvents.addAll(uiEchoManager.getInMemorySendingEvents().filterEventsWithSettings()) + builtSendingEvents.addAll( + uiEchoManager.getInMemorySendingEvents() + .filterEventsWithSettings(settings) + ) + sendingEvents .map { timelineEventMapper.map(it) } // Filter out sending event that are not displayable! - .filterEventsWithSettings() + .filterEventsWithSettings(settings) .forEach { timelineEvent -> if (builtSendingEvents.find { it.eventId == timelineEvent.eventId } == null) { uiEchoManager.updateSentStateWithUiEcho(timelineEvent) @@ -432,14 +443,14 @@ internal class DefaultTimeline( return isReady.get() && !getState(direction).isPaginating && hasMoreToLoad(direction) } - private fun getState(direction: Timeline.Direction): State { + private fun getState(direction: Timeline.Direction): TimelineState { return when (direction) { Timeline.Direction.FORWARDS -> forwardsState.get() Timeline.Direction.BACKWARDS -> backwardsState.get() } } - private fun updateState(direction: Timeline.Direction, update: (State) -> State) { + private fun updateState(direction: Timeline.Direction, update: (TimelineState) -> TimelineState) { val stateReference = when (direction) { Timeline.Direction.FORWARDS -> forwardsState Timeline.Direction.BACKWARDS -> backwardsState @@ -511,7 +522,7 @@ internal class DefaultTimeline( eventEntity?.eventId?.let { eventId -> postSnapshot = rebuildEvent(eventId) { val builtEvent = buildTimelineEvent(eventEntity) - listOf(builtEvent).filterEventsWithSettings().firstOrNull() + listOf(builtEvent).filterEventsWithSettings(settings).firstOrNull() } || postSnapshot } } @@ -744,8 +755,8 @@ internal class DefaultTimeline( nextDisplayIndex = null builtEvents.clear() builtEventsIdMap.clear() - backwardsState.set(State()) - forwardsState.set(State()) + backwardsState.set(TimelineState()) + forwardsState.set(TimelineState()) } private fun createPaginationCallback(limit: Int, direction: Timeline.Direction): MatrixCallback { @@ -779,191 +790,4 @@ internal class DefaultTimeline( private fun Timeline.Direction.toPaginationDirection(): PaginationDirection { return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS } - - private fun RealmQuery.filterEventsWithSettings(): RealmQuery { - return filterEvents(settings.filters) - } - - private fun List.filterEventsWithSettings(): List { - return filter { event -> - val filterType = !settings.filters.filterTypes - || settings.filters.allowedTypes.any { it.eventType == event.root.type && (it.stateKey == null || it.stateKey == event.root.senderId) } - if (!filterType) return@filter false - - val filterEdits = if (settings.filters.filterEdits && event.root.getClearType() == EventType.MESSAGE) { - val messageContent = event.root.getClearContent().toModel() - messageContent?.relatesTo?.type != RelationType.REPLACE && messageContent?.relatesTo?.type != RelationType.RESPONSE - } else { - true - } - if (!filterEdits) return@filter false - - val filterRedacted = settings.filters.filterRedacted && event.root.isRedacted() - !filterRedacted - } - } - - private data class State( - val hasReachedEnd: Boolean = false, - val hasMoreInCache: Boolean = true, - 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.toList() - } - - /** - * 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.forEach { (_, uiEchoData) -> - uiEchoData.removeAll { data -> - // I remove the uiEcho, when the related event is not anymore in the sending list - // (means that it is synced)! - events.find { it.eventId == data.localEchoId } == null - } - } - } - - fun onLocalEchoUpdated(roomId: String, eventId: String, sendState: SendState): Boolean { - if (isLive && roomId == this@DefaultTimeline.roomId) { - val existingState = inMemorySendingStates[eventId] - inMemorySendingStates[eventId] = sendState - if (existingState != sendState) { - return true - } - } - return false - } - - // return true if should update - fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent): Boolean { - var postSnapshot = false - if (isLive && roomId == this@DefaultTimeline.roomId) { - // Manage some ui echos (do it before filter because actual event could be filtered out) - when (timelineEvent.root.getClearType()) { - EventType.REDACTION -> { - } - EventType.REACTION -> { - val content = 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 = timelineEvent.eventId, - reactedOnEventId = relatedEventID, - reaction = reaction - ) - ) - postSnapshot = rebuildEvent(relatedEventID) { - decorateEventWithReactionUiEcho(it) - } || postSnapshot - } - } - } - - // do not add events that would have been filtered - if (listOf(timelineEvent).filterEventsWithSettings().isNotEmpty()) { - listeners.forEach { - it.onNewTimelineEvents(listOf(timelineEvent.eventId)) - } - Timber.v("On local echo created: ${timelineEvent.eventId}") - inMemorySendingEvents.add(0, 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 } - } - } - } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/Extensions.kt new file mode 100644 index 0000000000..b2c8021f3b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/Extensions.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.timeline + +import io.realm.RealmQuery +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.message.MessageContent +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.query.filterEvents + +internal fun RealmQuery.filterEventsWithSettings(settings: TimelineSettings): RealmQuery { + return filterEvents(settings.filters) +} + +internal fun List.filterEventsWithSettings(settings: TimelineSettings): List { + return filter { event -> + val filterType = !settings.filters.filterTypes + || settings.filters.allowedTypes.any { it.eventType == event.root.type && (it.stateKey == null || it.stateKey == event.root.senderId) } + if (!filterType) return@filter false + + val filterEdits = if (settings.filters.filterEdits && event.root.getClearType() == EventType.MESSAGE) { + val messageContent = event.root.getClearContent().toModel() + messageContent?.relatesTo?.type != RelationType.REPLACE && messageContent?.relatesTo?.type != RelationType.RESPONSE + } else { + true + } + if (!filterEdits) return@filter false + + val filterRedacted = settings.filters.filterRedacted && event.root.isRedacted() + !filterRedacted + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/ReactionUiEchoData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/ReactionUiEchoData.kt new file mode 100644 index 0000000000..521120c415 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/ReactionUiEchoData.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.timeline + +internal data class ReactionUiEchoData( + val localEchoId: String, + val reactedOnEventId: String, + val reaction: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineState.kt new file mode 100644 index 0000000000..0143d9bab3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineState.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.timeline + +internal data class TimelineState( + val hasReachedEnd: Boolean = false, + val hasMoreInCache: Boolean = true, + val isPaginating: Boolean = false, + val requestedPaginationCount: Int = 0 +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt new file mode 100644 index 0000000000..f37532744d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.timeline + +import io.realm.RealmResults +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.relation.ReactionContent +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import timber.log.Timber +import java.util.Collections + +internal class UIEchoManager( + private val settings: TimelineSettings, + private val listener: Listener +) { + + interface Listener { + fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent?): Boolean + } + + private val inMemorySendingEvents = Collections.synchronizedList(ArrayList()) + + fun getInMemorySendingEvents(): List { + return inMemorySendingEvents.toList() + } + + /** + * 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.forEach { (_, uiEchoData) -> + uiEchoData.removeAll { data -> + // I remove the uiEcho, when the related event is not anymore in the sending list + // (means that it is synced)! + events.find { it.eventId == data.localEchoId } == null + } + } + } + + fun onLocalEchoUpdated(eventId: String, sendState: SendState): Boolean { + val existingState = inMemorySendingStates[eventId] + inMemorySendingStates[eventId] = sendState + return existingState != sendState + } + + // return true if should update + fun onLocalEchoCreated(timelineEvent: TimelineEvent): Boolean { + var postSnapshot = false + + // Manage some ui echos (do it before filter because actual event could be filtered out) + when (timelineEvent.root.getClearType()) { + EventType.REDACTION -> { + } + EventType.REACTION -> { + val content = 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 = timelineEvent.eventId, + reactedOnEventId = relatedEventID, + reaction = reaction + ) + ) + postSnapshot = listener.rebuildEvent(relatedEventID) { + decorateEventWithReactionUiEcho(it) + } || postSnapshot + } + } + } + + // do not add events that would have been filtered + if (listOf(timelineEvent).filterEventsWithSettings(settings).isNotEmpty()) { + Timber.v("On local echo created: ${timelineEvent.eventId}") + inMemorySendingEvents.add(0, 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}") + if(element.root.sendState != SendState.SENT) { + element.root.sendState = 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 } + } + } +}