From 99925d7cf97fec6d5f0192a773ae24bee65a7fd9 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 29 May 2019 18:43:33 +0200 Subject: [PATCH] Local echo for reactions/edits/redacts --- matrix-sdk-android/build.gradle | 2 +- .../session/room/timeline/TimelineTest.kt | 5 +- .../room/model/EditAggregatedSummary.kt | 1 + .../room/model/ReactionAggregatedSummary.kt | 3 +- .../mapper/EventAnnotationsSummaryMapper.kt | 4 +- .../model/EditAggregatedSummaryEntity.kt | 1 + .../model/ReactionAggregatedSummaryEntity.kt | 4 +- .../database/query/EventEntityQueries.kt | 29 +- .../android/internal/session/SessionModule.kt | 10 +- .../room/EventRelationsAggregationTask.kt | 309 ++++++++++++++++++ .../room/EventRelationsAggregationUpdater.kt | 222 ++----------- .../internal/session/room/RoomModule.kt | 16 +- .../session/room/prune/EventsPruner.kt | 7 +- .../session/room/prune/PruneEventTask.kt | 30 +- .../room/relation/DefaultRelationService.kt | 111 +++---- .../room/relation/SendRelationWorker.kt | 6 +- .../session/room/send/DefaultSendService.kt | 50 +-- .../room/send/LocalEchoEventFactory.kt | 86 ++++- .../session/room/send/RedactEventWorker.kt | 20 +- .../session/room/send/SendEventWorker.kt | 6 +- .../timeline/TimelineSendEventWorkCommon.kt | 56 ++++ .../room/timeline/TokenChunkEventPersistor.kt | 18 +- .../internal/session/sync/RoomSyncHandler.kt | 24 +- .../internal/session/sync/SyncModule.kt | 2 +- .../timeline/factory/MessageItemFactory.kt | 4 +- .../detail/timeline/item/AbsMessageItem.kt | 9 +- .../timeline/item/MessageInformationData.kt | 16 +- 27 files changed, 658 insertions(+), 393 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineSendEventWorkCommon.kt diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 5211a223..1a0c0f91 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -48,7 +48,7 @@ android { buildConfigField "boolean", "LOG_PRIVATE_DATA", "false" // Set to BODY instead of NONE to enable logging - buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.NONE" + buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.BASIC" } release { diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt index e81e481d..221887c7 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt @@ -18,12 +18,10 @@ package im.vector.matrix.android.session.room.timeline import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.InstrumentedTest -import im.vector.matrix.android.api.auth.data.Credentials 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.internal.database.model.SessionRealmModule import im.vector.matrix.android.internal.session.room.EventRelationExtractor -import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater import im.vector.matrix.android.internal.session.room.membership.SenderRoomMemberExtractor import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFactory @@ -60,8 +58,7 @@ internal class TimelineTest : InstrumentedTest { private fun createTimeline(initialEventId: String? = null): Timeline { val taskExecutor = TaskExecutor(testCoroutineDispatchers) - val erau = EventRelationsAggregationUpdater(Credentials("", "", "", null, null)) - val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy, erau) + val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy) val paginationTask = FakePaginationTask(tokenChunkEventPersistor) val getContextOfEventTask = FakeGetContextOfEventTask(tokenChunkEventPersistor) val roomMemberExtractor = SenderRoomMemberExtractor(ROOM_ID) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/EditAggregatedSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/EditAggregatedSummary.kt index 636343a5..759ba1ec 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/EditAggregatedSummary.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/EditAggregatedSummary.kt @@ -21,5 +21,6 @@ data class EditAggregatedSummary( val aggregatedContent: Content? = null, // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk) val sourceEvents: List, + val localEchos: List, val lastEditTs: Long = 0 ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReactionAggregatedSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReactionAggregatedSummary.kt index a6948cd1..1fbe58c7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReactionAggregatedSummary.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReactionAggregatedSummary.kt @@ -5,5 +5,6 @@ data class ReactionAggregatedSummary( val count: Int, // 8 val addedByMe: Boolean, // true val firstTimestamp: Long, // unix timestamp - val sourceEvents: List + val sourceEvents: List, + val localEchoEvents: List ) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventAnnotationsSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventAnnotationsSummaryMapper.kt index 2d1a7da1..8b653b74 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventAnnotationsSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventAnnotationsSummaryMapper.kt @@ -15,13 +15,15 @@ internal object EventAnnotationsSummaryMapper { it.count, it.addedByMe, it.firstTimestamp, - it.sourceEvents.toList() + it.sourceEvents.toList(), + it.sourceLocalEcho.toList() ) }, editSummary = annotationsSummary.editSummary?.let { EditAggregatedSummary( ContentMapper.map(it.aggregatedContent), it.sourceEvents.toList(), + it.sourceLocalEchoEvents.toList(), it.lastEditTs ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EditAggregatedSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EditAggregatedSummaryEntity.kt index 4b2a43e8..f2690c96 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EditAggregatedSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EditAggregatedSummaryEntity.kt @@ -25,6 +25,7 @@ internal open class EditAggregatedSummaryEntity( var aggregatedContent: String? = null, // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk) var sourceEvents: RealmList = RealmList(), + var sourceLocalEchoEvents: RealmList = RealmList(), var lastEditTs: Long = 0 ) : RealmObject() { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReactionAggregatedSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReactionAggregatedSummaryEntity.kt index 93ec8bd7..4b94313d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReactionAggregatedSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReactionAggregatedSummaryEntity.kt @@ -16,7 +16,9 @@ internal open class ReactionAggregatedSummaryEntity( // The first time this reaction was added (for ordering purpose) var firstTimestamp: Long = 0, // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk) - var sourceEvents: RealmList = RealmList() + var sourceEvents: RealmList = RealmList(), + // List of transaction ids for local echos + var sourceLocalEcho: RealmList = RealmList() ) : RealmObject() { companion object diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt index c500a880..c366ff75 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.internal.database.query +import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntity.LinkFilterMode.* @@ -42,12 +43,36 @@ internal fun EventEntity.Companion.where(realm: Realm, query.equalTo(EventEntityFields.TYPE, type) } return when (linkFilterMode) { - LINKED_ONLY -> query.equalTo(EventEntityFields.IS_UNLINKED, false) + LINKED_ONLY -> query.equalTo(EventEntityFields.IS_UNLINKED, false) UNLINKED_ONLY -> query.equalTo(EventEntityFields.IS_UNLINKED, true) - BOTH -> query + BOTH -> query } } +//internal fun EventEntity.Companion.unsent(realm: Realm, +// roomId: String? = null): RealmQuery { +// val query = realm.where() +// if (roomId != null) { +// query.equalTo(EventEntityFields.ROOM_ID, roomId) +// } +// query.equalTo(EventEntityFields.SEND_STATE_STR, SendState.UNSENT.name) +// return query +//} +// +//internal fun EventEntity.Companion.byTypes(realm: Realm, +// types: List): RealmQuery { +// val query = realm.where() +// types.forEachIndexed { index, type -> +// if (index == 0) { +// query.equalTo(EventEntityFields.TYPE, type) +// } else { +// query.or().equalTo(EventEntityFields.TYPE, type) +// } +// } +// return query +//} + + internal fun EventEntity.Companion.latestEvent(realm: Realm, roomId: String, includedTypes: List = emptyList(), diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index 6d8084c0..8f8ee454 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -105,10 +105,6 @@ internal class SessionModule(private val sessionParams: SessionParams) { RoomSummaryUpdater(get(), get(), get()) } - scope(DefaultSession.SCOPE) { - EventRelationsAggregationUpdater(get()) - } - scope(DefaultSession.SCOPE) { DefaultRoomService(get(), get(), get(), get()) as RoomService } @@ -168,9 +164,11 @@ internal class SessionModule(private val sessionParams: SessionParams) { scope(DefaultSession.SCOPE) { val groupSummaryUpdater = GroupSummaryUpdater(get()) - val eventsPruner = EventsPruner(get(), get(), get(), get()) val userEntityUpdater = UserEntityUpdater(get(), get(), get()) - listOf(groupSummaryUpdater, eventsPruner, userEntityUpdater) + val aggregationUpdater = EventRelationsAggregationUpdater(get(), get(), get(), get()) + //Event pruner must be the last one, because it will clear contents + val eventsPruner = EventsPruner(get(), get(), get(), get()) + listOf(groupSummaryUpdater, userEntityUpdater, aggregationUpdater, eventsPruner) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt new file mode 100644 index 00000000..5181bb30 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt @@ -0,0 +1,309 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.session.room + +import arrow.core.Try +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.session.events.model.* +import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.relation.ReactionContent +import im.vector.matrix.android.api.session.room.send.SendState +import im.vector.matrix.android.internal.database.mapper.ContentMapper +import im.vector.matrix.android.internal.database.mapper.EventMapper +import im.vector.matrix.android.internal.database.model.* +import im.vector.matrix.android.internal.database.query.create +import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.task.Task +import im.vector.matrix.android.internal.util.tryTransactionAsync +import io.realm.Realm +import timber.log.Timber + +internal interface EventRelationsAggregationTask : Task { + + data class Params( + val events: List>, + val userId: String + ) +} + +/** + * Called by EventRelationAggregationUpdater, when new events that can affect relations are inserted in base. + */ +internal class DefaultEventRelationsAggregationTask(private val monarchy: Monarchy) : EventRelationsAggregationTask { + + override fun execute(params: EventRelationsAggregationTask.Params): Try { + return monarchy.tryTransactionAsync { realm -> + update(realm, params.events, params.userId) + } + } + + fun update(realm: Realm, events: List>?, userId: String) { + events?.forEach { pair -> + val roomId = pair.first.roomId ?: return@forEach + val event = pair.first + val sendState = pair.second + val isLocalEcho = sendState == SendState.UNSENT + when (event.type) { + EventType.REACTION -> { + //we got a reaction!! + Timber.v("###REACTION in room $roomId") + handleReaction(event, roomId, realm, userId, isLocalEcho) + } + EventType.MESSAGE -> { + if (event.unsignedData?.relations?.annotations != null) { + Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}") + handleInitialAggregatedRelations(event, roomId, event.unsignedData.relations.annotations, realm) + } else { + val content: MessageContent? = event.content.toModel() + if (content?.relatesTo?.type == RelationType.REPLACE) { + Timber.v("###REPLACE in room $roomId for event ${event.eventId}") + //A replace! + handleReplace(realm, event, content, roomId, isLocalEcho) + } + } + + } + EventType.REDACTION -> { + val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() } + ?: return + when (eventToPrune.type) { + EventType.MESSAGE -> { + Timber.d("REDACTION for message ${eventToPrune.eventId}") + val unsignedData = EventMapper.map(eventToPrune).unsignedData + ?: UnsignedData(null, null) + + //was this event a m.replace + val contentModel = ContentMapper.map(eventToPrune.content)?.toModel() + if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) { + handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm) + } + + } + EventType.REACTION -> { + handleReactionRedact(eventToPrune, realm, userId) + } + } + } + } + } + } + + private fun handleReplace(realm: Realm, event: Event, content: MessageContent, roomId: String, isLocalEcho: Boolean) { + val eventId = event.eventId ?: return + val targetEventId = content.relatesTo?.eventId ?: return + val newContent = content.newContent ?: return + //ok, this is a replace + var existing = EventAnnotationsSummaryEntity.where(realm, targetEventId).findFirst() + if (existing == null) { + Timber.v("###REPLACE creating no relation summary for ${targetEventId}") + existing = EventAnnotationsSummaryEntity.create(realm, targetEventId) + existing.roomId = roomId + } + + //we have it + val existingSummary = existing.editSummary + if (existingSummary == null) { + Timber.v("###REPLACE no edit summary for ${targetEventId}, creating one (localEcho:$isLocalEcho)") + //create the edit summary + val editSummary = realm.createObject(EditAggregatedSummaryEntity::class.java) + editSummary.lastEditTs = event.originServerTs ?: System.currentTimeMillis() + editSummary.aggregatedContent = ContentMapper.map(newContent) + if (isLocalEcho) { + editSummary.sourceLocalEchoEvents.add(eventId) + } else { + editSummary.sourceEvents.add(eventId) + } + + existing.editSummary = editSummary + } else { + if (existingSummary.sourceEvents.contains(eventId)) { + //ignore this event, we already know it (??) + Timber.v("###REPLACE ignoring event for summary, it's known ${eventId}") + return + } + val txId = event.unsignedData?.transactionId + //is it a remote echo? + if (!isLocalEcho && existingSummary.sourceLocalEchoEvents.contains(txId)) { + //ok it has already been managed + Timber.v("###REPLACE Receiving remote echo of edit (edit already done)") + existingSummary.sourceLocalEchoEvents.remove(txId) + existingSummary.sourceEvents.add(event.eventId) + } else if (event.originServerTs ?: 0 > existingSummary.lastEditTs) { + Timber.v("###REPLACE Computing aggregated edit summary (isLocalEcho:$isLocalEcho)") + existingSummary.lastEditTs = event.originServerTs ?: System.currentTimeMillis() + existingSummary.aggregatedContent = ContentMapper.map(newContent) + existingSummary.sourceEvents.add(eventId) + } else { + //ignore this event for the summary + Timber.v("###REPLACE ignoring event for summary, it's to old ${eventId}") + } + } + + } + + private fun handleInitialAggregatedRelations(event: Event, roomId: String, aggregation: AggregatedAnnotation, realm: Realm) { + aggregation.chunk?.forEach { + if (it.type == EventType.REACTION) { + val eventId = event.eventId ?: "" + val existing = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() + if (existing == null) { + val eventSummary = EventAnnotationsSummaryEntity.create(realm, eventId) + eventSummary.roomId = roomId + val sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java) + sum.key = it.key + sum.firstTimestamp = event.originServerTs ?: 0 //TODO how to maintain order? + sum.count = it.count + eventSummary.reactionsSummary.add(sum) + } else { + //TODO how to handle that + } + } + } + } + + fun handleReaction(event: Event, roomId: String, realm: Realm, userId: String, isLocalEcho: Boolean) { + event.content.toModel()?.let { content -> + //rel_type must be m.annotation + if (RelationType.ANNOTATION == content.relatesTo?.type) { + val reaction = content.relatesTo.key + val eventId = content.relatesTo.eventId + val eventSummary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() + ?: EventAnnotationsSummaryEntity.create(realm, eventId).apply { this.roomId = roomId } + + var sum = eventSummary.reactionsSummary.find { it.key == reaction } + val txId = event.unsignedData?.transactionId + if (isLocalEcho && txId.isNullOrBlank()) { + Timber.w("Received a local echo with no transaction ID") + } + if (sum == null) { + sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java) + sum.key = reaction + sum.firstTimestamp = event.originServerTs ?: 0 + if (isLocalEcho) { + Timber.v("Adding local echo reaction $reaction") + sum.sourceLocalEcho.add(txId) + sum.count = 1 + } else { + Timber.v("Adding synced reaction $reaction") + sum.count = 1 + sum.sourceEvents.add(event.eventId) + } + sum.addedByMe = sum.addedByMe || (userId == event.sender) + eventSummary.reactionsSummary.add(sum) + } else { + //is this a known event (is possible? pagination?) + if (!sum.sourceEvents.contains(eventId)) { + + //check if it's not the sync of a local echo + if (!isLocalEcho && sum.sourceLocalEcho.contains(txId)) { + //ok it has already been counted, just sync the list, do not touch count + Timber.v("Ignoring synced of local echo for reaction $reaction") + sum.sourceLocalEcho.remove(txId) + sum.sourceEvents.add(event.eventId) + } else { + sum.count += 1 + if (isLocalEcho) { + Timber.v("Adding local echo reaction $reaction") + sum.sourceLocalEcho.add(txId) + } else { + Timber.v("Adding synced reaction $reaction") + sum.sourceEvents.add(event.eventId) + } + + sum.addedByMe = sum.addedByMe || (userId == event.sender) + } + + } + } + + } + } + } + + /** + * Called when an event is deleted + */ + fun handleRedactionOfReplace(redacted: EventEntity, relatedEventId: String, realm: Realm) { + Timber.d("Handle redaction of m.replace") + val eventSummary = EventAnnotationsSummaryEntity.where(realm, relatedEventId).findFirst() + if (eventSummary == null) { + Timber.w("Redaction of a replace targeting an unknown event $relatedEventId") + return + } + val sourceEvents = eventSummary.editSummary?.sourceEvents + val sourceToDiscard = sourceEvents?.indexOf(redacted.eventId) + if (sourceToDiscard == null) { + Timber.w("Redaction of a replace that was not known in aggregation $sourceToDiscard") + return + } + //Need to remove this event from the redaction list and compute new aggregation state + sourceEvents.removeAt(sourceToDiscard) + val previousEdit = sourceEvents.mapNotNull { EventEntity.where(realm, it).findFirst() }.sortedBy { it.originServerTs }.lastOrNull() + if (previousEdit == null) { + //revert to original + eventSummary.editSummary?.deleteFromRealm() + } else { + //I have the last event + ContentMapper.map(previousEdit.content)?.toModel()?.newContent?.let { newContent -> + eventSummary.editSummary?.lastEditTs = previousEdit.originServerTs + ?: System.currentTimeMillis() + eventSummary.editSummary?.aggregatedContent = ContentMapper.map(newContent) + } ?: run { + Timber.e("Failed to udate edited summary") + //TODO how to reccover that + } + + } + } + + fun handleReactionRedact(eventToPrune: EventEntity, realm: Realm, userId: String) { + Timber.d("REDACTION of reaction ${eventToPrune.eventId}") + //delete a reaction, need to update the annotation summary if any + val reactionContent: ReactionContent = EventMapper.map(eventToPrune).content.toModel() + ?: return + val eventThatWasReacted = reactionContent.relatesTo?.eventId ?: return + + val reactionkey = reactionContent.relatesTo.key + Timber.d("REMOVE reaction for key $reactionkey") + val summary = EventAnnotationsSummaryEntity.where(realm, eventThatWasReacted).findFirst() + if (summary != null) { + summary.reactionsSummary.where() + .equalTo(ReactionAggregatedSummaryEntityFields.KEY, reactionkey) + .findFirst()?.let { summary -> + Timber.d("Find summary for key with ${summary.sourceEvents.size} known reactions (count:${summary.count})") + Timber.d("Known reactions ${summary.sourceEvents.joinToString(",")}") + if (summary.sourceEvents.contains(eventToPrune.eventId)) { + Timber.d("REMOVE reaction for key $reactionkey") + summary.sourceEvents.remove(eventToPrune.eventId) + Timber.d("Known reactions after ${summary.sourceEvents.joinToString(",")}") + summary.count = summary.count - 1 + if (eventToPrune.sender == userId) { + //Was it a redact on my reaction? + summary.addedByMe = false + } + if (summary.count == 0) { + //delete! + summary.deleteFromRealm() + } + } else { + Timber.e("## Cannot remove summary from count, corresponding reaction ${eventToPrune.eventId} is not known") + } + } + } else { + Timber.e("## Cannot find summary for key $reactionkey") + } + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt index 64802553..0b29064c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt @@ -15,16 +15,14 @@ */ package im.vector.matrix.android.internal.session.room +import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.auth.data.Credentials -import im.vector.matrix.android.api.session.events.model.* -import im.vector.matrix.android.api.session.room.model.relation.ReactionContent -import im.vector.matrix.android.api.session.room.model.message.MessageContent -import im.vector.matrix.android.internal.database.mapper.ContentMapper -import im.vector.matrix.android.internal.database.mapper.EventMapper -import im.vector.matrix.android.internal.database.model.* -import im.vector.matrix.android.internal.database.query.create +import im.vector.matrix.android.internal.database.RealmLiveEntityObserver +import im.vector.matrix.android.internal.database.mapper.asDomain +import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.query.where -import io.realm.Realm +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith import timber.log.Timber /** @@ -32,198 +30,34 @@ import timber.log.Timber * For reactions will build a EventAnnotationsSummaryEntity, ans for edits a EditAggregatedSummaryEntity. * The summaries can then be extracted and added (as a decoration) to a TimelineEvent for final display. */ -internal class EventRelationsAggregationUpdater(private val credentials: Credentials) { +internal class EventRelationsAggregationUpdater(monarchy: Monarchy, + private val credentials: Credentials, + private val task: EventRelationsAggregationTask, + private val taskExecutor: TaskExecutor) : + RealmLiveEntityObserver(monarchy) { - fun update(realm: Realm, roomId: String, events: List?) { - events?.forEach { event -> - when (event.type) { - EventType.REACTION -> { - //we got a reaction!! - Timber.v("###REACTION in room $roomId") - handleReaction(event, roomId, realm) - } - EventType.MESSAGE -> { - if (event.unsignedData?.relations?.annotations != null) { - Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}") - handleInitialAggregatedRelations(event, roomId, event.unsignedData.relations.annotations, realm) - } else { - val content: MessageContent? = event.content.toModel() - if (content?.relatesTo?.type == RelationType.REPLACE) { - Timber.v("###REPLACE in room $roomId for event ${event.eventId}") - //A replace! - handleReplace(event, content, roomId, realm) - } - } - } - } - } + override val query = Monarchy.Query { + EventEntity.where(it) + //mmm why is this query not working? +// EventEntity.byTypes(it, listOf( +// EventType.REDACTION, EventType.MESSAGE, EventType.REDACTION) +// ) } - private fun handleReplace(event: Event, content: MessageContent, roomId: String, realm: Realm) { - val eventId = event.eventId ?: return - val targetEventId = content.relatesTo?.eventId ?: return - val newContent = content.newContent ?: return - //ok, this is a replace - var existing = EventAnnotationsSummaryEntity.where(realm, targetEventId).findFirst() - if (existing == null) { - Timber.v("###REPLACE creating no relation summary for ${targetEventId}") - existing = EventAnnotationsSummaryEntity.create(realm, targetEventId) - existing.roomId = roomId - } + override fun processChanges(inserted: List, updated: List, deleted: List) { + Timber.v("EventRelationsAggregationUpdater called with ${inserted.size} insertions") + val inserted = inserted + .mapNotNull { it.asDomain() to it.sendState } - //we have it - val existingSummary = existing.editSummary - if (existingSummary == null) { - Timber.v("###REPLACE no edit summary for ${targetEventId}, creating one") - //create the edit summary - val editSummary = realm.createObject(EditAggregatedSummaryEntity::class.java) - editSummary.lastEditTs = event.originServerTs ?: System.currentTimeMillis() - editSummary.aggregatedContent = ContentMapper.map(newContent) - editSummary.sourceEvents.add(eventId) + val params = EventRelationsAggregationTask.Params( + inserted, + credentials.userId + ) - existing.editSummary = editSummary - } else { - if (existingSummary.sourceEvents.contains(eventId)) { - //ignore this event, we already know it (??) - Timber.v("###REPLACE ignoring event for summary, it's known ${eventId}") - return - } - //This message has already been edited - if (event.originServerTs ?: 0 > existingSummary.lastEditTs ?: 0) { - Timber.v("###REPLACE Computing aggregated edit summary") - existingSummary.lastEditTs = event.originServerTs ?: System.currentTimeMillis() - existingSummary.aggregatedContent = ContentMapper.map(newContent) - existingSummary.sourceEvents.add(eventId) - } else { - //ignore this event for the summary - Timber.v("###REPLACE ignoring event for summary, it's to old ${eventId}") - } - } + task.configureWith(params) + .executeBy(taskExecutor) } - private fun handleInitialAggregatedRelations(event: Event, roomId: String, aggregation: AggregatedAnnotation, realm: Realm) { - aggregation.chunk?.forEach { - if (it.type == EventType.REACTION) { - val eventId = event.eventId ?: "" - val existing = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() - if (existing == null) { - val eventSummary = EventAnnotationsSummaryEntity.create(realm, eventId) - eventSummary.roomId = roomId - val sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java) - sum.key = it.key - sum.firstTimestamp = event.originServerTs ?: 0 //TODO how to maintain order? - sum.count = it.count - eventSummary.reactionsSummary.add(sum) - } else { - //TODO how to handle that - } - } - } - } +} - fun handleReaction(event: Event, roomId: String, realm: Realm) { - event.content.toModel()?.let { content -> - //rel_type must be m.annotation - if (RelationType.ANNOTATION == content.relatesTo?.type) { - val reaction = content.relatesTo.key - val eventId = content.relatesTo.eventId - val eventSummary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() - ?: EventAnnotationsSummaryEntity.create(realm, eventId).apply { this.roomId = roomId } - - var sum = eventSummary.reactionsSummary.find { it.key == reaction } - if (sum == null) { - sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java) - sum.key = reaction - sum.firstTimestamp = event.originServerTs ?: 0 - sum.count = 1 - sum.sourceEvents.add(event.eventId) - sum.addedByMe = sum.addedByMe || (credentials.userId == event.sender) - eventSummary.reactionsSummary.add(sum) - } else { - //is this a known event (is possible? pagination?) - if (!sum.sourceEvents.contains(eventId)) { - sum.count += 1 - sum.sourceEvents.add(event.eventId) - sum.addedByMe = sum.addedByMe || (credentials.userId == event.sender) - } - } - - } - } - } - - /** - * Called when an event is deleted - */ - fun handleRedactionOfReplace(redacted: EventEntity, relatedEventId: String, realm: Realm) { - Timber.d("Handle redaction of m.replace") - val eventSummary = EventAnnotationsSummaryEntity.where(realm, relatedEventId).findFirst() - if (eventSummary == null) { - Timber.w("Redaction of a replace targeting an unknown event $relatedEventId") - return - } - val sourceEvents = eventSummary.editSummary?.sourceEvents - val sourceToDiscard = sourceEvents?.indexOf(redacted.eventId) - if (sourceToDiscard == null) { - Timber.w("Redaction of a replace that was not known in aggregation $sourceToDiscard") - return - } - //Need to remove this event from the redaction list and compute new aggregation state - sourceEvents.removeAt(sourceToDiscard) - val previousEdit = sourceEvents.mapNotNull { EventEntity.where(realm, it).findFirst() }.sortedBy { it.originServerTs }.lastOrNull() - if (previousEdit == null) { - //revert to original - eventSummary.editSummary?.deleteFromRealm() - } else { - //I have the last event - ContentMapper.map(previousEdit.content)?.toModel()?.newContent?.let { newContent -> - eventSummary.editSummary?.lastEditTs = previousEdit.originServerTs - ?: System.currentTimeMillis() - eventSummary.editSummary?.aggregatedContent = ContentMapper.map(newContent) - } ?: run { - Timber.e("Failed to udate edited summary") - //TODO how to reccover that - } - - } - } - - fun handleReactionRedact(eventToPrune: EventEntity, realm: Realm, userId: String) { - Timber.d("REDACTION of reaction ${eventToPrune.eventId}") - //delete a reaction, need to update the annotation summary if any - val reactionContent: ReactionContent = EventMapper.map(eventToPrune).content.toModel() - ?: return - val eventThatWasReacted = reactionContent.relatesTo?.eventId ?: return - - val reactionkey = reactionContent.relatesTo.key - Timber.d("REMOVE reaction for key $reactionkey") - val summary = EventAnnotationsSummaryEntity.where(realm, eventThatWasReacted).findFirst() - if (summary != null) { - summary.reactionsSummary.where() - .equalTo(ReactionAggregatedSummaryEntityFields.KEY, reactionkey) - .findFirst()?.let { summary -> - Timber.d("Find summary for key with ${summary.sourceEvents.size} known reactions (count:${summary.count})") - Timber.d("Known reactions ${summary.sourceEvents.joinToString(",")}") - if (summary.sourceEvents.contains(eventToPrune.eventId)) { - Timber.d("REMOVE reaction for key $reactionkey") - summary.sourceEvents.remove(eventToPrune.eventId) - Timber.d("Known reactions after ${summary.sourceEvents.joinToString(",")}") - summary.count = summary.count - 1 - if (eventToPrune.sender == userId) { - //Was it a redact on my reaction? - summary.addedByMe = false - } - if (summary.count == 0) { - //delete! - summary.deleteFromRealm() - } - } else { - Timber.e("## Cannot remove summary from count, corresponding reaction ${eventToPrune.eventId} is not known") - } - } - } else { - Timber.e("## Cannot find summary for key $reactionkey") - } - } -} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt index 7013b99d..7f47f576 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt @@ -17,10 +17,6 @@ package im.vector.matrix.android.internal.session.room import im.vector.matrix.android.internal.session.DefaultSession -import im.vector.matrix.android.internal.session.room.relation.DefaultFindReactionEventForUndoTask -import im.vector.matrix.android.internal.session.room.relation.DefaultUpdateQuickReactionTask -import im.vector.matrix.android.internal.session.room.relation.FindReactionEventForUndoTask -import im.vector.matrix.android.internal.session.room.relation.UpdateQuickReactionTask import im.vector.matrix.android.internal.session.room.create.CreateRoomTask import im.vector.matrix.android.internal.session.room.create.DefaultCreateRoomTask import im.vector.matrix.android.internal.session.room.membership.DefaultLoadRoomMembersTask @@ -35,6 +31,10 @@ import im.vector.matrix.android.internal.session.room.prune.DefaultPruneEventTas import im.vector.matrix.android.internal.session.room.prune.PruneEventTask import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask +import im.vector.matrix.android.internal.session.room.relation.DefaultFindReactionEventForUndoTask +import im.vector.matrix.android.internal.session.room.relation.DefaultUpdateQuickReactionTask +import im.vector.matrix.android.internal.session.room.relation.FindReactionEventForUndoTask +import im.vector.matrix.android.internal.session.room.relation.UpdateQuickReactionTask import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory import im.vector.matrix.android.internal.session.room.state.DefaultSendStateTask import im.vector.matrix.android.internal.session.room.state.SendStateTask @@ -57,7 +57,7 @@ class RoomModule { } scope(DefaultSession.SCOPE) { - TokenChunkEventPersistor(get(), get()) + TokenChunkEventPersistor(get()) } scope(DefaultSession.SCOPE) { @@ -109,7 +109,11 @@ class RoomModule { } scope(DefaultSession.SCOPE) { - DefaultPruneEventTask(get(), get()) as PruneEventTask + DefaultPruneEventTask(get()) as PruneEventTask + } + + scope(DefaultSession.SCOPE) { + DefaultEventRelationsAggregationTask(get()) as EventRelationsAggregationTask } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt index cb7a3818..4d3bc6fd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt @@ -25,8 +25,12 @@ import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith +import timber.log.Timber - +/** + * Listens to the database for the insertion of any redaction event. + * As it will actually delete the content, it should be called last in the list of listener. + */ internal class EventsPruner(monarchy: Monarchy, private val credentials: Credentials, private val pruneEventTask: PruneEventTask, @@ -36,6 +40,7 @@ internal class EventsPruner(monarchy: Monarchy, override val query = Monarchy.Query { EventEntity.where(it, type = EventType.REDACTION) } override fun processChanges(inserted: List, updated: List, deleted: List) { + Timber.v("Event pruner called with ${inserted.size} insertions") val redactionEvents = inserted .mapNotNull { it.asDomain() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt index 44c62a09..a4025978 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt @@ -17,14 +17,15 @@ package im.vector.matrix.android.internal.session.room.prune import arrow.core.Try import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.api.session.events.model.* -import im.vector.matrix.android.api.session.room.model.message.MessageContent +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.UnsignedData +import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.internal.database.mapper.ContentMapper import im.vector.matrix.android.internal.database.mapper.EventMapper import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.di.MoshiProvider -import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.tryTransactionSync import io.realm.Realm @@ -41,8 +42,7 @@ internal interface PruneEventTask : Task { } internal class DefaultPruneEventTask( - private val monarchy: Monarchy, - private val eventRelationsAggregationUpdater: EventRelationsAggregationUpdater) : PruneEventTask { + private val monarchy: Monarchy) : PruneEventTask { override fun execute(params: PruneEventTask.Params): Try { return monarchy.tryTransactionSync { realm -> @@ -57,6 +57,12 @@ internal class DefaultPruneEventTask( return } + val redactionEventEntity = EventEntity.where(realm, eventId = redactionEvent.eventId + ?: "").findFirst() + ?: return + val isLocalEcho = redactionEventEntity.sendState == SendState.UNSENT + Timber.v("Redact event for ${redactionEvent.redacts} localEcho=$isLocalEcho") + val eventToPrune = EventEntity.where(realm, eventId = redactionEvent.redacts).findFirst() ?: return @@ -72,19 +78,19 @@ internal class DefaultPruneEventTask( ?: UnsignedData(null, null) //was this event a m.replace - val contentModel = ContentMapper.map(eventToPrune.content)?.toModel() - if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) { - eventRelationsAggregationUpdater.handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm) - } +// val contentModel = ContentMapper.map(eventToPrune.content)?.toModel() +// if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) { +// eventRelationsAggregationUpdater.handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm) +// } val modified = unsignedData.copy(redactedEvent = redactionEvent) eventToPrune.content = ContentMapper.map(emptyMap()) eventToPrune.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified) } - EventType.REACTION -> { - eventRelationsAggregationUpdater.handleReactionRedact(eventToPrune, realm, userId) - } +// EventType.REACTION -> { +// eventRelationsAggregationUpdater.handleReactionRedact(eventToPrune, realm, userId) +// } } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt index 0c2addd5..6d6e4763 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt @@ -15,12 +15,12 @@ */ package im.vector.matrix.android.internal.session.room.relation -import androidx.work.* +import androidx.work.OneTimeWorkRequest import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.room.model.relation.RelationService import im.vector.matrix.android.api.session.room.model.message.MessageType +import im.vector.matrix.android.api.session.room.model.relation.RelationService import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.database.helper.addSendingEvent import im.vector.matrix.android.internal.database.model.ChunkEntity @@ -30,19 +30,14 @@ import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory import im.vector.matrix.android.internal.session.room.send.RedactEventWorker import im.vector.matrix.android.internal.session.room.send.SendEventWorker +import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEventWorkCommon import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.CancelableWork import im.vector.matrix.android.internal.util.WorkerParamsFactory import im.vector.matrix.android.internal.util.tryTransactionAsync -import java.util.concurrent.TimeUnit +import timber.log.Timber -private const val REACTION_WORK = "REACTION_WORK" -private const val BACKOFF_DELAY = 10_000L - -private val WORK_CONSTRAINTS = Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build() internal class DefaultRelationService(private val roomId: String, private val eventFactory: LocalEchoEventFactory, @@ -55,28 +50,22 @@ internal class DefaultRelationService(private val roomId: String, override fun sendReaction(reaction: String, targetEventId: String): Cancelable { val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction) -// .also { -// //saveLocalEcho(it) -// } + .also { + saveLocalEcho(it) + } val sendRelationWork = createSendRelationWork(event) - WorkManager.getInstance() - .beginUniqueWork(buildWorkIdentifier(REACTION_WORK), ExistingWorkPolicy.APPEND, sendRelationWork) - .enqueue() + TimelineSendEventWorkCommon.postWork(roomId, sendRelationWork) return CancelableWork(sendRelationWork.id) } private fun createSendRelationWork(event: Event): OneTimeWorkRequest { - //TODO use the new API to send relation (for now use regular send) val sendContentWorkerParams = SendEventWorker.Params( roomId, event) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) - return OneTimeWorkRequestBuilder() - .setConstraints(WORK_CONSTRAINTS) - .setInputData(sendWorkData) - .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) - .build() + return TimelineSendEventWorkCommon.createWork(sendWorkData) + } override fun undoReaction(reaction: String, targetEventId: String, myUserId: String)/*: Cancelable*/ { @@ -91,11 +80,19 @@ internal class DefaultRelationService(private val roomId: String, .enableRetry() .dispatchTo(object : MatrixCallback { override fun onSuccess(data: FindReactionEventForUndoTask.Result) { + if (data.redactEventId == null) { + Timber.w("Cannot find reaction to undo (not yet synced?)") + //TODO? + } data.redactEventId?.let { toRedact -> - val redactWork = createRedactEventWork(toRedact, null) - WorkManager.getInstance() - .beginUniqueWork(buildWorkIdentifier(REACTION_WORK), ExistingWorkPolicy.APPEND, redactWork) - .enqueue() + + val redactEvent = eventFactory.createRedactEvent(roomId, toRedact, null).also { + saveLocalEcho(it) + } + val redactWork = createRedactEventWork(redactEvent, toRedact, null) + + TimelineSendEventWorkCommon.postWork(roomId, redactWork) + } } }) @@ -119,10 +116,11 @@ internal class DefaultRelationService(private val roomId: String, override fun onSuccess(data: UpdateQuickReactionTask.Result) { data.reactionToAdd?.also { sendReaction(it, targetEventId) } data.reactionToRedact.forEach { - val redactWork = createRedactEventWork(it, null) - WorkManager.getInstance() - .beginUniqueWork(buildWorkIdentifier(REACTION_WORK), ExistingWorkPolicy.APPEND, redactWork) - .enqueue() + val redactEvent = eventFactory.createRedactEvent(roomId, it, null).also { + saveLocalEcho(it) + } + val redactWork = createRedactEventWork(redactEvent, it, null) + TimelineSendEventWorkCommon.postWork(roomId, redactWork) } } }) @@ -133,48 +131,26 @@ internal class DefaultRelationService(private val roomId: String, return "${roomId}_$identifier" } -// private fun saveLocalEcho(event: Event) { -// monarchy.tryTransactionAsync { realm -> -// val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() -// ?: return@tryTransactionAsync -// val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId = roomId) -// ?: return@tryTransactionAsync -// -// roomEntity.addSendingEvent(event, liveChunk.forwardsStateIndex ?: 0) -// } -// } - //TODO duplicate with send service? - private fun createRedactEventWork(eventId: String, reason: String?): OneTimeWorkRequest { + private fun createRedactEventWork(localEvent: Event, eventId: String, reason: String?): OneTimeWorkRequest { - //TODO create local echo of m.room.redaction event? - - val sendContentWorkerParams = RedactEventWorker.Params( + val sendContentWorkerParams = RedactEventWorker.Params(localEvent.eventId!!, roomId, eventId, reason) val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) - - return OneTimeWorkRequestBuilder() - .setConstraints(WORK_CONSTRAINTS) - .setInputData(redactWorkData) - .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) - .build() + return TimelineSendEventWorkCommon.createWork(redactWorkData) } override fun editTextMessage(targetEventId: String, newBodyText: String, newBodyAutoMarkdown: Boolean, compatibilityBodyText: String): Cancelable { - val event = eventFactory.createReplaceTextEvent(roomId, targetEventId, newBodyText, newBodyAutoMarkdown, MessageType.MSGTYPE_TEXT, compatibilityBodyText) + val event = eventFactory.createReplaceTextEvent(roomId, targetEventId, newBodyText, newBodyAutoMarkdown, MessageType.MSGTYPE_TEXT, compatibilityBodyText).also { + saveLocalEcho(it) + } val sendContentWorkerParams = SendEventWorker.Params(roomId, event) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) //TODO use relation API? - val workRequest = OneTimeWorkRequestBuilder() - .setConstraints(WORK_CONSTRAINTS) - .setInputData(sendWorkData) - .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) - .build() - WorkManager.getInstance() - .beginUniqueWork(buildWorkIdentifier(REACTION_WORK), ExistingWorkPolicy.APPEND, workRequest) - .enqueue() + val workRequest = TimelineSendEventWorkCommon.createWork(sendWorkData) + TimelineSendEventWorkCommon.postWork(roomId, workRequest) return CancelableWork(workRequest.id) } @@ -186,18 +162,19 @@ internal class DefaultRelationService(private val roomId: String, } ?: return null val sendContentWorkerParams = SendEventWorker.Params(roomId, event) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) - val workRequest = OneTimeWorkRequestBuilder() - .setConstraints(WORK_CONSTRAINTS) - .setInputData(sendWorkData) - .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) - .build() - WorkManager.getInstance() - .beginUniqueWork(buildWorkIdentifier(REACTION_WORK), ExistingWorkPolicy.APPEND, workRequest) - .enqueue() + + val workRequest = TimelineSendEventWorkCommon.createWork(sendWorkData) + TimelineSendEventWorkCommon.postWork(roomId, workRequest) return CancelableWork(workRequest.id) } + /** + * Saves the event in database as a local echo. + * SendState is set to UNSENT and it's added to a the sendingTimelineEvents list of the room. + * The sendingTimelineEvents is checked on new sync and will remove the local echo if an event with + * the same transaction id is received (in unsigned data) + */ private fun saveLocalEcho(event: Event) { monarchy.tryTransactionAsync { realm -> val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/SendRelationWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/SendRelationWorker.kt index 8fac4ed2..41184aaf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/SendRelationWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/SendRelationWorker.kt @@ -70,7 +70,11 @@ class SendRelationWorker(context: Context, params: WorkerParameters) return result.fold({ when (it) { is Failure.NetworkConnection -> Result.retry() - else -> Result.failure() + else -> { + //TODO mark as failed to send? + //always return success, or the chain will be stuck for ever! + Result.success() + } } }, { Result.success() }) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt index 6b16ffaf..d5b8a1c7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt @@ -24,18 +24,12 @@ import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.CancelableBag import im.vector.matrix.android.api.util.addTo -import im.vector.matrix.android.internal.database.helper.addSendingEvent -import im.vector.matrix.android.internal.database.model.ChunkEntity -import im.vector.matrix.android.internal.database.model.RoomEntity -import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom -import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.content.UploadContentWorker +import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEventWorkCommon import im.vector.matrix.android.internal.util.CancelableWork import im.vector.matrix.android.internal.util.WorkerParamsFactory -import im.vector.matrix.android.internal.util.tryTransactionAsync import java.util.concurrent.TimeUnit -private const val SEND_WORK = "SEND_WORK" private const val UPLOAD_WORK = "UPLOAD_WORK" private const val BACKOFF_DELAY = 10_000L @@ -54,9 +48,7 @@ internal class DefaultSendService(private val roomId: String, saveLocalEcho(it) } val sendWork = createSendEventWork(event) - WorkManager.getInstance() - .beginUniqueWork(buildWorkIdentifier(SEND_WORK), ExistingWorkPolicy.APPEND, sendWork) - .enqueue() + TimelineSendEventWorkCommon.postWork(roomId, sendWork) return CancelableWork(sendWork.id) } @@ -65,9 +57,7 @@ internal class DefaultSendService(private val roomId: String, saveLocalEcho(it) } val sendWork = createSendEventWork(event) - WorkManager.getInstance() - .beginUniqueWork(buildWorkIdentifier(SEND_WORK), ExistingWorkPolicy.APPEND, sendWork) - .enqueue() + TimelineSendEventWorkCommon.postWork(roomId, sendWork) return CancelableWork(sendWork.id) } @@ -80,12 +70,9 @@ internal class DefaultSendService(private val roomId: String, } override fun redactEvent(event: Event, reason: String?): Cancelable { - //TODO manage local echo ? //TODO manage media/attachements? val redactWork = createRedactEventWork(event, reason) - WorkManager.getInstance() - .beginUniqueWork(buildWorkIdentifier(SEND_WORK), ExistingWorkPolicy.APPEND, redactWork) - .enqueue() + TimelineSendEventWorkCommon.postWork(roomId, redactWork) return CancelableWork(redactWork.id) } @@ -106,14 +93,7 @@ internal class DefaultSendService(private val roomId: String, } private fun saveLocalEcho(event: Event) { - monarchy.tryTransactionAsync { realm -> - val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() - ?: return@tryTransactionAsync - val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId = roomId) - ?: return@tryTransactionAsync - - roomEntity.addSendingEvent(event, liveChunk.forwardsStateIndex ?: 0) - } + eventFactory.saveLocalEcho(monarchy, event) } private fun buildWorkIdentifier(identifier: String): String { @@ -124,26 +104,20 @@ internal class DefaultSendService(private val roomId: String, val sendContentWorkerParams = SendEventWorker.Params(roomId, event) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) - return OneTimeWorkRequestBuilder() - .setConstraints(WORK_CONSTRAINTS) - .setInputData(sendWorkData) - .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) - .build() + return TimelineSendEventWorkCommon.createWork(sendWorkData) } private fun createRedactEventWork(event: Event, reason: String?): OneTimeWorkRequest { - //TODO create local echo of m.room.redaction event? + val redactEvent = eventFactory.createRedactEvent(roomId, event.eventId!!, reason).also { + saveLocalEcho(it) + } - val sendContentWorkerParams = RedactEventWorker.Params( - roomId, event.eventId!!, reason) + val sendContentWorkerParams = RedactEventWorker.Params(redactEvent.eventId!!, + roomId, event.eventId, reason) val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) - return OneTimeWorkRequestBuilder() - .setConstraints(WORK_CONSTRAINTS) - .setInputData(redactWorkData) - .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) - .build() + return TimelineSendEventWorkCommon.createWork(redactWorkData) } private fun createUploadMediaWork(event: Event, attachment: ContentAttachmentData): OneTimeWorkRequest { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index a810baac..582862d6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -18,21 +18,37 @@ package im.vector.matrix.android.internal.session.room.send import android.media.MediaMetadataRetriever import android.text.TextUtils +import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.R import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.events.model.* +import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.model.relation.ReactionContent import im.vector.matrix.android.api.session.room.model.relation.ReactionInfo import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent -import im.vector.matrix.android.api.session.room.model.message.* +import im.vector.matrix.android.internal.database.helper.addSendingEvent +import im.vector.matrix.android.internal.database.model.ChunkEntity +import im.vector.matrix.android.internal.database.model.RoomEntity +import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom +import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.content.ThumbnailExtractor import im.vector.matrix.android.internal.util.StringProvider +import im.vector.matrix.android.internal.util.tryTransactionAsync import org.commonmark.parser.Parser import org.commonmark.renderer.html.HtmlRenderer +/** + * Creates local echo of events for room events. + * A local echo is an event that is persisted even if not yet sent to the server, + * in an optimistic way (as if the server as responded immediately). Local echo are using a local id, + * (the transaction ID), this id is used when receiving an event from a sync to check if this event + * is matching an existing local echo. + * + * The transactionID is used as loc + */ internal class LocalEchoEventFactory(private val credentials: Credentials, private val stringProvider: StringProvider) { fun createTextEvent(roomId: String, msgType: String, text: String, autoMarkdown: Boolean): Event { @@ -41,7 +57,7 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva val document = parser.parse(text) val renderer = HtmlRenderer.builder().build() val htmlText = renderer.render(document) - if (!TextUtils.equals(text, htmlText)) { + if (isFormattedTextPertinent(text, htmlText)) { //FIX that return createFormattedTextEvent(roomId, text, htmlText) } } @@ -49,6 +65,9 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva return createEvent(roomId, content) } + private fun isFormattedTextPertinent(text: String, htmlText: String?) = + text != htmlText && htmlText != "

$text

\n" + fun createFormattedTextEvent(roomId: String, text: String, formattedText: String): Event { val content = MessageTextContent( type = MessageType.MSGTYPE_TEXT, @@ -71,7 +90,7 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva val document = parser.parse(newBodyText) val renderer = HtmlRenderer.builder().build() val htmlText = renderer.render(document) - if (!TextUtils.equals(newBodyText, htmlText)) { + if (isFormattedTextPertinent(newBodyText, htmlText)) { newContent = MessageTextContent( type = MessageType.MSGTYPE_TEXT, format = MessageType.FORMAT_MATRIX_HTML, @@ -107,14 +126,16 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva reaction ) ) + val localId = dummyEventId(roomId) return Event( roomId = roomId, originServerTs = dummyOriginServerTs(), sender = credentials.userId, - eventId = dummyEventId(roomId), + eventId = localId, type = EventType.REACTION, - content = content.toContent() - ) + content = content.toContent(), + unsignedData = UnsignedData(age = null, transactionId = localId)) + } @@ -196,13 +217,15 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva } private fun createEvent(roomId: String, content: Any? = null): Event { + val localID = dummyEventId(roomId) return Event( roomId = roomId, originServerTs = dummyOriginServerTs(), sender = credentials.userId, - eventId = dummyEventId(roomId), + eventId = localID, type = EventType.MESSAGE, - content = content.toContent() + content = content.toContent(), + unsignedData = UnsignedData(age = null, transactionId = localID) ) } @@ -211,7 +234,7 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva } private fun dummyEventId(roomId: String): String { - return roomId + "-" + dummyOriginServerTs() + return "m.${txNCounter++}" } fun createReplyTextEvent(roomId: String, eventReplied: Event, replyText: String): Event? { @@ -285,4 +308,49 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva } } + + /* + * { + "content": { + "reason": "Spamming" + }, + "event_id": "$143273582443PhrSn:domain.com", + "origin_server_ts": 1432735824653, + "redacts": "$fukweghifu23:localhost", + "room_id": "!jEsUZKDJdhlrceRyVU:domain.com", + "sender": "@example:domain.com", + "type": "m.room.redaction", + "unsigned": { + "age": 1234 + } + } + */ + fun createRedactEvent(roomId: String, eventId: String, reason: String?): Event { + val localID = dummyEventId(roomId) + return Event( + roomId = roomId, + originServerTs = dummyOriginServerTs(), + sender = credentials.userId, + eventId = localID, + type = EventType.REDACTION, + redacts = eventId, + content = reason?.let { mapOf("reason" to it).toContent() }, + unsignedData = UnsignedData(age = null, transactionId = localID) + ) + } + + fun saveLocalEcho(monarchy: Monarchy, event: Event) { + monarchy.tryTransactionAsync { realm -> + val roomEntity = RoomEntity.where(realm, roomId = event.roomId!!).findFirst() + ?: return@tryTransactionAsync + val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId = event.roomId) + ?: return@tryTransactionAsync + + roomEntity.addSendingEvent(event, liveChunk.forwardsStateIndex ?: 0) + } + } + + companion object { + var txNCounter = System.currentTimeMillis() + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/RedactEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/RedactEventWorker.kt index e0bc740e..5056a93b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/RedactEventWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/RedactEventWorker.kt @@ -25,13 +25,13 @@ import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.util.WorkerParamsFactory import org.koin.standalone.inject -import java.util.* internal class RedactEventWorker(context: Context, params: WorkerParameters) : Worker(context, params), MatrixKoinComponent { @JsonClass(generateAdapter = true) internal data class Params( + val txID: String, val roomId: String, val eventId: String, val reason: String? @@ -40,26 +40,26 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters) private val roomAPI by inject() override fun doWork(): Result { - val params = WorkerParamsFactory.fromData(inputData) + val params = WorkerParamsFactory.fromData(inputData) ?: return Result.failure() - if (params.eventId == null) { - return Result.failure() - } - val txID = UUID.randomUUID().toString() - + val eventId = params.eventId val result = executeRequest { apiCall = roomAPI.redactEvent( - txID, + params.txID, params.roomId, - params.eventId, + eventId, if (params.reason == null) emptyMap() else mapOf("reason" to params.reason) ) } return result.fold({ when (it) { is Failure.NetworkConnection -> Result.retry() - else -> Result.failure() + else -> { + //TODO mark as failed to send? + //always return success, or the chain will be stuck for ever! + Result.success() + } } }, { Result.success() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt index 73330770..f9a0478b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt @@ -61,7 +61,11 @@ internal class SendEventWorker(context: Context, params: WorkerParameters) return result.fold({ when (it) { is Failure.NetworkConnection -> Result.retry() - else -> Result.failure() + else -> { + //TODO mark as failed to send? + //always return success, or the chain will be stuck for ever! + Result.success() + } } }, { Result.success() }) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineSendEventWorkCommon.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineSendEventWorkCommon.kt new file mode 100644 index 00000000..8205f2cc --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineSendEventWorkCommon.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.session.room.timeline + +import androidx.work.* +import java.util.concurrent.TimeUnit + + +private const val SEND_WORK = "SEND_WORK" +private const val BACKOFF_DELAY = 10_000L + +private val WORK_CONSTRAINTS = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + +/** + * Helper class for sending event related works. + * All send event from a room are using the same workchain, in order to ensure order. + * WorkRequest must always return success (even if server error, in this case marking the event as failed to send) + * , if not the chain will be doomed in failed state. + * + */ +internal object TimelineSendEventWorkCommon { + + fun postWork(roomId: String, workRequest: OneTimeWorkRequest) { + WorkManager.getInstance() + .beginUniqueWork(buildWorkIdentifier(roomId), ExistingWorkPolicy.APPEND, workRequest) + .enqueue() + + } + + inline fun createWork(data: Data): OneTimeWorkRequest { + return OneTimeWorkRequestBuilder() + .setConstraints(WORK_CONSTRAINTS) + .setInputData(data) + .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) + .build() + } + + private fun buildWorkIdentifier(roomId: String): String { + return "${roomId}_$SEND_WORK" + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt index ec85c558..9c40b4aa 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt @@ -18,20 +18,13 @@ package im.vector.matrix.android.internal.session.room.timeline import arrow.core.Try import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.internal.database.helper.addAll -import im.vector.matrix.android.internal.database.helper.addOrUpdate -import im.vector.matrix.android.internal.database.helper.addStateEvents -import im.vector.matrix.android.internal.database.helper.deleteOnCascade -import im.vector.matrix.android.internal.database.helper.isUnlinked -import im.vector.matrix.android.internal.database.helper.merge -import im.vector.matrix.android.internal.database.mapper.EventMapper +import im.vector.matrix.android.internal.database.helper.* import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.query.create import im.vector.matrix.android.internal.database.query.find import im.vector.matrix.android.internal.database.query.findAllIncludingEvents import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater import im.vector.matrix.android.internal.util.tryTransactionSync import io.realm.kotlin.createObject import timber.log.Timber @@ -39,8 +32,7 @@ import timber.log.Timber /** * Insert Chunk in DB, and eventually merge with existing chunk event */ -internal class TokenChunkEventPersistor(private val monarchy: Monarchy, - private val eventRelationsAggregationUpdater: EventRelationsAggregationUpdater) { +internal class TokenChunkEventPersistor(private val monarchy: Monarchy) { /** *
@@ -119,7 +111,7 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy,
                     Timber.v("Start persisting ${receivedChunk.events.size} events in $roomId towards $direction")
 
                     val roomEntity = RoomEntity.where(realm, roomId).findFirst()
-                                     ?: realm.createObject(roomId)
+                            ?: realm.createObject(roomId)
 
                     val nextToken: String?
                     val prevToken: String?
@@ -142,7 +134,7 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy,
                     } else {
                         nextChunk?.apply { this.prevToken = prevToken }
                     }
-                                       ?: ChunkEntity.create(realm, prevToken, nextToken)
+                            ?: ChunkEntity.create(realm, prevToken, nextToken)
 
                     if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) {
                         Timber.v("Reach end of $roomId")
@@ -151,8 +143,6 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy,
                         Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}")
                         currentChunk.addAll(roomId, receivedChunk.events, direction, isUnlinked = currentChunk.isUnlinked())
 
-                        //Event
-                        eventRelationsAggregationUpdater.update(realm,roomId,receivedChunk.events.toList())
                         // Then we merge chunks if needed
                         if (currentChunk != prevChunk && prevChunk != null) {
                             currentChunk = handleMerge(roomEntity, direction, currentChunk, prevChunk)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt
index eaab9846..018cc518 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt
@@ -31,14 +31,9 @@ import im.vector.matrix.android.internal.database.model.RoomEntity
 import im.vector.matrix.android.internal.database.query.find
 import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
 import im.vector.matrix.android.internal.database.query.where
-import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
 import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
 import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
-import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync
-import im.vector.matrix.android.internal.session.sync.model.RoomSync
-import im.vector.matrix.android.internal.session.sync.model.RoomSyncAccountData
-import im.vector.matrix.android.internal.session.sync.model.RoomSyncEphemeral
-import im.vector.matrix.android.internal.session.sync.model.RoomsSyncResponse
+import im.vector.matrix.android.internal.session.sync.model.*
 import io.realm.Realm
 import io.realm.kotlin.createObject
 import timber.log.Timber
@@ -46,8 +41,7 @@ import timber.log.Timber
 internal class RoomSyncHandler(private val monarchy: Monarchy,
                                private val readReceiptHandler: ReadReceiptHandler,
                                private val roomSummaryUpdater: RoomSummaryUpdater,
-                               private val roomTagHandler: RoomTagHandler,
-                               private val eventRelationsAggregationUpdater: EventRelationsAggregationUpdater) {
+                               private val roomTagHandler: RoomTagHandler) {
 
     sealed class HandlingStrategy {
         data class JOINED(val data: Map) : HandlingStrategy()
@@ -67,9 +61,9 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
 
     private fun handleRoomSync(realm: Realm, handlingStrategy: HandlingStrategy) {
         val rooms = when (handlingStrategy) {
-            is HandlingStrategy.JOINED  -> handlingStrategy.data.map { handleJoinedRoom(realm, it.key, it.value) }
+            is HandlingStrategy.JOINED -> handlingStrategy.data.map { handleJoinedRoom(realm, it.key, it.value) }
             is HandlingStrategy.INVITED -> handlingStrategy.data.map { handleInvitedRoom(realm, it.key, it.value) }
-            is HandlingStrategy.LEFT    -> handlingStrategy.data.map { handleLeftRoom(realm, it.key, it.value) }
+            is HandlingStrategy.LEFT -> handlingStrategy.data.map { handleLeftRoom(realm, it.key, it.value) }
         }
         realm.insertOrUpdate(rooms)
     }
@@ -81,7 +75,7 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
         Timber.v("Handle join sync for room $roomId")
 
         val roomEntity = RoomEntity.where(realm, roomId).findFirst()
-                         ?: realm.createObject(roomId)
+                ?: realm.createObject(roomId)
 
         if (roomEntity.membership == Membership.INVITE) {
             roomEntity.chunks.deleteAllFromRealm()
@@ -116,11 +110,13 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
             transactionIds.forEach {
                 val sendingEventEntity = roomEntity.sendingTimelineEvents.find(it)
                 if (sendingEventEntity != null) {
+                    Timber.v("Remove local echo for tx:$it")
                     roomEntity.sendingTimelineEvents.remove(sendingEventEntity)
+                } else {
+                    Timber.v("Can't find corresponding local echo for tx:$it")
                 }
             }
         }
-        eventRelationsAggregationUpdater.update(realm, roomId, roomSync.timeline?.events)
         roomSummaryUpdater.update(realm, roomId, Membership.JOIN, roomSync.summary, roomSync.unreadNotifications)
 
         if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) {
@@ -139,7 +135,7 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
                                   InvitedRoomSync): RoomEntity {
         Timber.v("Handle invited sync for room $roomId")
         val roomEntity = RoomEntity.where(realm, roomId).findFirst()
-                         ?: realm.createObject(roomId)
+                ?: realm.createObject(roomId)
         roomEntity.membership = Membership.INVITE
         if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) {
             val chunkEntity = handleTimelineEvents(realm, roomId, roomSync.inviteState.events)
@@ -153,7 +149,7 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
                                roomId: String,
                                roomSync: RoomSync): RoomEntity {
         val roomEntity = RoomEntity.where(realm, roomId).findFirst()
-                         ?: realm.createObject(roomId)
+                ?: realm.createObject(roomId)
 
         roomEntity.membership = Membership.LEAVE
         roomEntity.chunks.deleteAllFromRealm()
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncModule.kt
index d6014061..d0f60405 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncModule.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncModule.kt
@@ -40,7 +40,7 @@ internal class SyncModule {
         }
 
         scope(DefaultSession.SCOPE) {
-            RoomSyncHandler(get(), get(), get(), get(), get())
+            RoomSyncHandler(get(), get(), get(), get())
         }
 
         scope(DefaultSession.SCOPE) {
diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt
index 05fda074..4509b19e 100644
--- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt
+++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt
@@ -90,7 +90,9 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
                 avatarUrl = avatarUrl,
                 memberName = formattedMemberName,
                 showInformation = showInformation,
-                orderedReactionList = event.annotations?.reactionsSummary?.map { Triple(it.key, it.count, it.addedByMe) },
+                orderedReactionList = event.annotations?.reactionsSummary?.map {
+                    ReactionInfoData(it.key, it.count, it.addedByMe, it.localEchoEvents.isEmpty())
+                },
                 hasBeenEdited = hasBeenEdited
         )
 
diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt
index d560424d..0ce01a88 100644
--- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt
+++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt
@@ -112,11 +112,12 @@ abstract class AbsMessageItem : BaseEventItem() {
                 (holder.reactionWrapper?.children?.elementAtOrNull(index) as? ReactionButton)?.let { reactionButton ->
                     reactionButton.isVisible = true
                     reactionButton.reactedListener = reactionClickListener
-                    reactionButton.setTag(R.id.messageBottomInfo, reaction.first)
+                    reactionButton.setTag(R.id.messageBottomInfo, reaction.key)
                     idToRefInFlow.add(reactionButton.id)
-                    reactionButton.reactionString = reaction.first
-                    reactionButton.reactionCount = reaction.second
-                    reactionButton.setChecked(reaction.third)
+                    reactionButton.reactionString = reaction.key
+                    reactionButton.reactionCount = reaction.count
+                    reactionButton.setChecked(reaction.addedByMe)
+                    reactionButton.isEnabled = reaction.synced
                 }
             }
             // Just setting the view as gone will break the FlowHelper (and invisible will take too much space),
diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageInformationData.kt
index 5cdd1f9c..e7f4aa82 100644
--- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageInformationData.kt
+++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageInformationData.kt
@@ -16,9 +16,8 @@
 
 package im.vector.riotredesign.features.home.room.detail.timeline.item
 
-import im.vector.matrix.android.api.session.room.send.SendState
-
 import android.os.Parcelable
+import im.vector.matrix.android.api.session.room.send.SendState
 import kotlinx.android.parcel.Parcelize
 
 @Parcelize
@@ -31,6 +30,15 @@ data class MessageInformationData(
         val memberName: CharSequence? = null,
         val showInformation: Boolean = true,
         /*List of reactions (emoji,count,isSelected)*/
-        var orderedReactionList: List>? = null,
+        var orderedReactionList: List? = null,
         var hasBeenEdited: Boolean = false
-) : Parcelable
\ No newline at end of file
+) : Parcelable
+
+
+@Parcelize
+data class ReactionInfoData(
+        val key: String,
+        val count: Int,
+        val addedByMe: Boolean,
+        val synced: Boolean
+) : Parcelable