Local echo for reactions/edits/redacts

This commit is contained in:
Valere 2019-05-29 18:43:33 +02:00
parent 466be1dca5
commit 99925d7cf9
27 changed files with 658 additions and 393 deletions

View File

@ -48,7 +48,7 @@ android {
buildConfigField "boolean", "LOG_PRIVATE_DATA", "false" buildConfigField "boolean", "LOG_PRIVATE_DATA", "false"


// Set to BODY instead of NONE to enable logging // 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 { release {

View File

@ -18,12 +18,10 @@ package im.vector.matrix.android.session.room.timeline


import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.InstrumentedTest 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.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.database.model.SessionRealmModule 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.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.membership.SenderRoomMemberExtractor
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline
import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFactory 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 { private fun createTimeline(initialEventId: String? = null): Timeline {
val taskExecutor = TaskExecutor(testCoroutineDispatchers) val taskExecutor = TaskExecutor(testCoroutineDispatchers)
val erau = EventRelationsAggregationUpdater(Credentials("", "", "", null, null)) val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy)
val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy, erau)
val paginationTask = FakePaginationTask(tokenChunkEventPersistor) val paginationTask = FakePaginationTask(tokenChunkEventPersistor)
val getContextOfEventTask = FakeGetContextOfEventTask(tokenChunkEventPersistor) val getContextOfEventTask = FakeGetContextOfEventTask(tokenChunkEventPersistor)
val roomMemberExtractor = SenderRoomMemberExtractor(ROOM_ID) val roomMemberExtractor = SenderRoomMemberExtractor(ROOM_ID)

View File

@ -21,5 +21,6 @@ data class EditAggregatedSummary(
val aggregatedContent: Content? = null, 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) // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk)
val sourceEvents: List<String>, val sourceEvents: List<String>,
val localEchos: List<String>,
val lastEditTs: Long = 0 val lastEditTs: Long = 0
) )

View File

@ -5,5 +5,6 @@ data class ReactionAggregatedSummary(
val count: Int, // 8 val count: Int, // 8
val addedByMe: Boolean, // true val addedByMe: Boolean, // true
val firstTimestamp: Long, // unix timestamp val firstTimestamp: Long, // unix timestamp
val sourceEvents: List<String> val sourceEvents: List<String>,
val localEchoEvents: List<String>
) )

View File

@ -15,13 +15,15 @@ internal object EventAnnotationsSummaryMapper {
it.count, it.count,
it.addedByMe, it.addedByMe,
it.firstTimestamp, it.firstTimestamp,
it.sourceEvents.toList() it.sourceEvents.toList(),
it.sourceLocalEcho.toList()
) )
}, },
editSummary = annotationsSummary.editSummary?.let { editSummary = annotationsSummary.editSummary?.let {
EditAggregatedSummary( EditAggregatedSummary(
ContentMapper.map(it.aggregatedContent), ContentMapper.map(it.aggregatedContent),
it.sourceEvents.toList(), it.sourceEvents.toList(),
it.sourceLocalEchoEvents.toList(),
it.lastEditTs it.lastEditTs
) )
} }

View File

@ -25,6 +25,7 @@ internal open class EditAggregatedSummaryEntity(
var aggregatedContent: String? = null, 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) // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk)
var sourceEvents: RealmList<String> = RealmList(), var sourceEvents: RealmList<String> = RealmList(),
var sourceLocalEchoEvents: RealmList<String> = RealmList(),
var lastEditTs: Long = 0 var lastEditTs: Long = 0
) : RealmObject() { ) : RealmObject() {



View File

@ -16,7 +16,9 @@ internal open class ReactionAggregatedSummaryEntity(
// The first time this reaction was added (for ordering purpose) // The first time this reaction was added (for ordering purpose)
var firstTimestamp: Long = 0, 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) // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk)
var sourceEvents: RealmList<String> = RealmList() var sourceEvents: RealmList<String> = RealmList(),
// List of transaction ids for local echos
var sourceLocalEcho: RealmList<String> = RealmList()
) : RealmObject() { ) : RealmObject() {


companion object companion object

View File

@ -16,6 +16,7 @@


package im.vector.matrix.android.internal.database.query 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.ChunkEntity
import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntity.LinkFilterMode.* 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) query.equalTo(EventEntityFields.TYPE, type)
} }
return when (linkFilterMode) { 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) UNLINKED_ONLY -> query.equalTo(EventEntityFields.IS_UNLINKED, true)
BOTH -> query BOTH -> query
} }
} }


//internal fun EventEntity.Companion.unsent(realm: Realm,
// roomId: String? = null): RealmQuery<EventEntity> {
// val query = realm.where<EventEntity>()
// 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<String>): RealmQuery<EventEntity> {
// val query = realm.where<EventEntity>()
// 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, internal fun EventEntity.Companion.latestEvent(realm: Realm,
roomId: String, roomId: String,
includedTypes: List<String> = emptyList(), includedTypes: List<String> = emptyList(),

View File

@ -105,10 +105,6 @@ internal class SessionModule(private val sessionParams: SessionParams) {
RoomSummaryUpdater(get(), get(), get()) RoomSummaryUpdater(get(), get(), get())
} }


scope(DefaultSession.SCOPE) {
EventRelationsAggregationUpdater(get())
}

scope(DefaultSession.SCOPE) { scope(DefaultSession.SCOPE) {
DefaultRoomService(get(), get(), get(), get()) as RoomService DefaultRoomService(get(), get(), get(), get()) as RoomService
} }
@ -168,9 +164,11 @@ internal class SessionModule(private val sessionParams: SessionParams) {


scope(DefaultSession.SCOPE) { scope(DefaultSession.SCOPE) {
val groupSummaryUpdater = GroupSummaryUpdater(get()) val groupSummaryUpdater = GroupSummaryUpdater(get())
val eventsPruner = EventsPruner(get(), get(), get(), get())
val userEntityUpdater = UserEntityUpdater(get(), get(), get()) val userEntityUpdater = UserEntityUpdater(get(), get(), get())
listOf<LiveEntityObserver>(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<LiveEntityObserver>(groupSummaryUpdater, userEntityUpdater, aggregationUpdater, eventsPruner)
} }





View File

@ -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<EventRelationsAggregationTask.Params, Unit> {

data class Params(
val events: List<Pair<Event, SendState>>,
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<Unit> {
return monarchy.tryTransactionAsync { realm ->
update(realm, params.events, params.userId)
}
}

fun update(realm: Realm, events: List<Pair<Event, SendState>>?, 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<MessageContent>()
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<ReactionContent>()?.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<MessageContent>()?.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")
}
}
}

View File

@ -15,16 +15,14 @@
*/ */
package im.vector.matrix.android.internal.session.room 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.auth.data.Credentials
import im.vector.matrix.android.api.session.events.model.* import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
import im.vector.matrix.android.api.session.room.model.relation.ReactionContent import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.internal.database.model.EventEntity
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.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 import timber.log.Timber


/** /**
@ -32,198 +30,34 @@ import timber.log.Timber
* For reactions will build a EventAnnotationsSummaryEntity, ans for edits a EditAggregatedSummaryEntity. * 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. * 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<EventEntity>(monarchy) {


fun update(realm: Realm, roomId: String, events: List<Event>?) { override val query = Monarchy.Query<EventEntity> {
events?.forEach { event -> EventEntity.where(it)
when (event.type) { //mmm why is this query not working?
EventType.REACTION -> { // EventEntity.byTypes(it, listOf(
//we got a reaction!! // EventType.REDACTION, EventType.MESSAGE, EventType.REDACTION)
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)
}
}
}
}
}
} }


private fun handleReplace(event: Event, content: MessageContent, roomId: String, realm: Realm) { override fun processChanges(inserted: List<EventEntity>, updated: List<EventEntity>, deleted: List<EventEntity>) {
val eventId = event.eventId ?: return Timber.v("EventRelationsAggregationUpdater called with ${inserted.size} insertions")
val targetEventId = content.relatesTo?.eventId ?: return val inserted = inserted
val newContent = content.newContent ?: return .mapNotNull { it.asDomain() to it.sendState }
//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 params = EventRelationsAggregationTask.Params(
val existingSummary = existing.editSummary inserted,
if (existingSummary == null) { credentials.userId
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)


existing.editSummary = editSummary task.configureWith(params)
} else { .executeBy(taskExecutor)
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}")
}
}


} }


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<ReactionContent>()?.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<MessageContent>()?.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")
}
}
} }


View File

@ -17,10 +17,6 @@
package im.vector.matrix.android.internal.session.room package im.vector.matrix.android.internal.session.room


import im.vector.matrix.android.internal.session.DefaultSession 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.CreateRoomTask
import im.vector.matrix.android.internal.session.room.create.DefaultCreateRoomTask import im.vector.matrix.android.internal.session.room.create.DefaultCreateRoomTask
import im.vector.matrix.android.internal.session.room.membership.DefaultLoadRoomMembersTask 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.prune.PruneEventTask
import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask 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.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.send.LocalEchoEventFactory
import im.vector.matrix.android.internal.session.room.state.DefaultSendStateTask import im.vector.matrix.android.internal.session.room.state.DefaultSendStateTask
import im.vector.matrix.android.internal.session.room.state.SendStateTask import im.vector.matrix.android.internal.session.room.state.SendStateTask
@ -57,7 +57,7 @@ class RoomModule {
} }


scope(DefaultSession.SCOPE) { scope(DefaultSession.SCOPE) {
TokenChunkEventPersistor(get(), get()) TokenChunkEventPersistor(get())
} }


scope(DefaultSession.SCOPE) { scope(DefaultSession.SCOPE) {
@ -109,7 +109,11 @@ class RoomModule {
} }


scope(DefaultSession.SCOPE) { scope(DefaultSession.SCOPE) {
DefaultPruneEventTask(get(), get()) as PruneEventTask DefaultPruneEventTask(get()) as PruneEventTask
}

scope(DefaultSession.SCOPE) {
DefaultEventRelationsAggregationTask(get()) as EventRelationsAggregationTask
} }


} }

View File

@ -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.database.query.where
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith 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, internal class EventsPruner(monarchy: Monarchy,
private val credentials: Credentials, private val credentials: Credentials,
private val pruneEventTask: PruneEventTask, private val pruneEventTask: PruneEventTask,
@ -36,6 +40,7 @@ internal class EventsPruner(monarchy: Monarchy,
override val query = Monarchy.Query<EventEntity> { EventEntity.where(it, type = EventType.REDACTION) } override val query = Monarchy.Query<EventEntity> { EventEntity.where(it, type = EventType.REDACTION) }


override fun processChanges(inserted: List<EventEntity>, updated: List<EventEntity>, deleted: List<EventEntity>) { override fun processChanges(inserted: List<EventEntity>, updated: List<EventEntity>, deleted: List<EventEntity>) {
Timber.v("Event pruner called with ${inserted.size} insertions")
val redactionEvents = inserted val redactionEvents = inserted
.mapNotNull { it.asDomain() } .mapNotNull { it.asDomain() }



View File

@ -17,14 +17,15 @@ package im.vector.matrix.android.internal.session.room.prune


import arrow.core.Try import arrow.core.Try
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.model.* import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.message.MessageContent 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.ContentMapper
import im.vector.matrix.android.internal.database.mapper.EventMapper 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.model.EventEntity
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.MoshiProvider 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.task.Task
import im.vector.matrix.android.internal.util.tryTransactionSync import im.vector.matrix.android.internal.util.tryTransactionSync
import io.realm.Realm import io.realm.Realm
@ -41,8 +42,7 @@ internal interface PruneEventTask : Task<PruneEventTask.Params, Unit> {
} }


internal class DefaultPruneEventTask( internal class DefaultPruneEventTask(
private val monarchy: Monarchy, private val monarchy: Monarchy) : PruneEventTask {
private val eventRelationsAggregationUpdater: EventRelationsAggregationUpdater) : PruneEventTask {


override fun execute(params: PruneEventTask.Params): Try<Unit> { override fun execute(params: PruneEventTask.Params): Try<Unit> {
return monarchy.tryTransactionSync { realm -> return monarchy.tryTransactionSync { realm ->
@ -57,6 +57,12 @@ internal class DefaultPruneEventTask(
return 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() val eventToPrune = EventEntity.where(realm, eventId = redactionEvent.redacts).findFirst()
?: return ?: return


@ -72,19 +78,19 @@ internal class DefaultPruneEventTask(
?: UnsignedData(null, null) ?: UnsignedData(null, null)


//was this event a m.replace //was this event a m.replace
val contentModel = ContentMapper.map(eventToPrune.content)?.toModel<MessageContent>() // val contentModel = ContentMapper.map(eventToPrune.content)?.toModel<MessageContent>()
if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) { // if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) {
eventRelationsAggregationUpdater.handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm) // eventRelationsAggregationUpdater.handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm)
} // }


val modified = unsignedData.copy(redactedEvent = redactionEvent) val modified = unsignedData.copy(redactedEvent = redactionEvent)
eventToPrune.content = ContentMapper.map(emptyMap()) eventToPrune.content = ContentMapper.map(emptyMap())
eventToPrune.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified) eventToPrune.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified)


} }
EventType.REACTION -> { // EventType.REACTION -> {
eventRelationsAggregationUpdater.handleReactionRedact(eventToPrune, realm, userId) // eventRelationsAggregationUpdater.handleReactionRedact(eventToPrune, realm, userId)
} // }
} }
} }
} }

View File

@ -15,12 +15,12 @@
*/ */
package im.vector.matrix.android.internal.session.room.relation package im.vector.matrix.android.internal.session.room.relation


import androidx.work.* import androidx.work.OneTimeWorkRequest
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.relation.RelationService
import im.vector.matrix.android.api.session.room.model.message.MessageType 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.api.util.Cancelable
import im.vector.matrix.android.internal.database.helper.addSendingEvent 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.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.LocalEchoEventFactory
import im.vector.matrix.android.internal.session.room.send.RedactEventWorker 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.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.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.CancelableWork import im.vector.matrix.android.internal.util.CancelableWork
import im.vector.matrix.android.internal.util.WorkerParamsFactory import im.vector.matrix.android.internal.util.WorkerParamsFactory
import im.vector.matrix.android.internal.util.tryTransactionAsync 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, internal class DefaultRelationService(private val roomId: String,
private val eventFactory: LocalEchoEventFactory, private val eventFactory: LocalEchoEventFactory,
@ -55,28 +50,22 @@ internal class DefaultRelationService(private val roomId: String,


override fun sendReaction(reaction: String, targetEventId: String): Cancelable { override fun sendReaction(reaction: String, targetEventId: String): Cancelable {
val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction) val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction)
// .also { .also {
// //saveLocalEcho(it) saveLocalEcho(it)
// } }
val sendRelationWork = createSendRelationWork(event) val sendRelationWork = createSendRelationWork(event)
WorkManager.getInstance() TimelineSendEventWorkCommon.postWork(roomId, sendRelationWork)
.beginUniqueWork(buildWorkIdentifier(REACTION_WORK), ExistingWorkPolicy.APPEND, sendRelationWork)
.enqueue()
return CancelableWork(sendRelationWork.id) return CancelableWork(sendRelationWork.id)
} }




private fun createSendRelationWork(event: Event): OneTimeWorkRequest { private fun createSendRelationWork(event: Event): OneTimeWorkRequest {
//TODO use the new API to send relation (for now use regular send)
val sendContentWorkerParams = SendEventWorker.Params( val sendContentWorkerParams = SendEventWorker.Params(
roomId, event) roomId, event)
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)


return OneTimeWorkRequestBuilder<SendEventWorker>() return TimelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData)
.setConstraints(WORK_CONSTRAINTS)
.setInputData(sendWorkData)
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
.build()
} }


override fun undoReaction(reaction: String, targetEventId: String, myUserId: String)/*: Cancelable*/ { override fun undoReaction(reaction: String, targetEventId: String, myUserId: String)/*: Cancelable*/ {
@ -91,11 +80,19 @@ internal class DefaultRelationService(private val roomId: String,
.enableRetry() .enableRetry()
.dispatchTo(object : MatrixCallback<FindReactionEventForUndoTask.Result> { .dispatchTo(object : MatrixCallback<FindReactionEventForUndoTask.Result> {
override fun onSuccess(data: FindReactionEventForUndoTask.Result) { 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 -> data.redactEventId?.let { toRedact ->
val redactWork = createRedactEventWork(toRedact, null)
WorkManager.getInstance() val redactEvent = eventFactory.createRedactEvent(roomId, toRedact, null).also {
.beginUniqueWork(buildWorkIdentifier(REACTION_WORK), ExistingWorkPolicy.APPEND, redactWork) saveLocalEcho(it)
.enqueue() }
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) { override fun onSuccess(data: UpdateQuickReactionTask.Result) {
data.reactionToAdd?.also { sendReaction(it, targetEventId) } data.reactionToAdd?.also { sendReaction(it, targetEventId) }
data.reactionToRedact.forEach { data.reactionToRedact.forEach {
val redactWork = createRedactEventWork(it, null) val redactEvent = eventFactory.createRedactEvent(roomId, it, null).also {
WorkManager.getInstance() saveLocalEcho(it)
.beginUniqueWork(buildWorkIdentifier(REACTION_WORK), ExistingWorkPolicy.APPEND, redactWork) }
.enqueue() val redactWork = createRedactEventWork(redactEvent, it, null)
TimelineSendEventWorkCommon.postWork(roomId, redactWork)
} }
} }
}) })
@ -133,48 +131,26 @@ internal class DefaultRelationService(private val roomId: String,
return "${roomId}_$identifier" 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? //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(localEvent.eventId!!,

val sendContentWorkerParams = RedactEventWorker.Params(
roomId, eventId, reason) roomId, eventId, reason)
val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)

return TimelineSendEventWorkCommon.createWork<RedactEventWorker>(redactWorkData)
return OneTimeWorkRequestBuilder<RedactEventWorker>()
.setConstraints(WORK_CONSTRAINTS)
.setInputData(redactWorkData)
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
.build()
} }


override fun editTextMessage(targetEventId: String, newBodyText: String, newBodyAutoMarkdown: Boolean, compatibilityBodyText: String): Cancelable { 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 sendContentWorkerParams = SendEventWorker.Params(roomId, event)
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)


//TODO use relation API? //TODO use relation API?
val workRequest = OneTimeWorkRequestBuilder<SendEventWorker>()
.setConstraints(WORK_CONSTRAINTS)
.setInputData(sendWorkData)
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
.build()


WorkManager.getInstance() val workRequest = TimelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData)
.beginUniqueWork(buildWorkIdentifier(REACTION_WORK), ExistingWorkPolicy.APPEND, workRequest) TimelineSendEventWorkCommon.postWork(roomId, workRequest)
.enqueue()
return CancelableWork(workRequest.id) return CancelableWork(workRequest.id)


} }
@ -186,18 +162,19 @@ internal class DefaultRelationService(private val roomId: String,
} ?: return null } ?: return null
val sendContentWorkerParams = SendEventWorker.Params(roomId, event) val sendContentWorkerParams = SendEventWorker.Params(roomId, event)
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
val workRequest = OneTimeWorkRequestBuilder<SendEventWorker>()
.setConstraints(WORK_CONSTRAINTS)
.setInputData(sendWorkData)
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
.build()


WorkManager.getInstance()
.beginUniqueWork(buildWorkIdentifier(REACTION_WORK), ExistingWorkPolicy.APPEND, workRequest) val workRequest = TimelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData)
.enqueue() TimelineSendEventWorkCommon.postWork(roomId, workRequest)
return CancelableWork(workRequest.id) 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) { private fun saveLocalEcho(event: Event) {
monarchy.tryTransactionAsync { realm -> monarchy.tryTransactionAsync { realm ->
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst()

View File

@ -70,7 +70,11 @@ class SendRelationWorker(context: Context, params: WorkerParameters)
return result.fold({ return result.fold({
when (it) { when (it) {
is Failure.NetworkConnection -> Result.retry() 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() }) }, { Result.success() })
} }

View File

@ -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.Cancelable
import im.vector.matrix.android.api.util.CancelableBag import im.vector.matrix.android.api.util.CancelableBag
import im.vector.matrix.android.api.util.addTo 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.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.CancelableWork
import im.vector.matrix.android.internal.util.WorkerParamsFactory import im.vector.matrix.android.internal.util.WorkerParamsFactory
import im.vector.matrix.android.internal.util.tryTransactionAsync
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit


private const val SEND_WORK = "SEND_WORK"
private const val UPLOAD_WORK = "UPLOAD_WORK" private const val UPLOAD_WORK = "UPLOAD_WORK"
private const val BACKOFF_DELAY = 10_000L private const val BACKOFF_DELAY = 10_000L


@ -54,9 +48,7 @@ internal class DefaultSendService(private val roomId: String,
saveLocalEcho(it) saveLocalEcho(it)
} }
val sendWork = createSendEventWork(event) val sendWork = createSendEventWork(event)
WorkManager.getInstance() TimelineSendEventWorkCommon.postWork(roomId, sendWork)
.beginUniqueWork(buildWorkIdentifier(SEND_WORK), ExistingWorkPolicy.APPEND, sendWork)
.enqueue()
return CancelableWork(sendWork.id) return CancelableWork(sendWork.id)
} }


@ -65,9 +57,7 @@ internal class DefaultSendService(private val roomId: String,
saveLocalEcho(it) saveLocalEcho(it)
} }
val sendWork = createSendEventWork(event) val sendWork = createSendEventWork(event)
WorkManager.getInstance() TimelineSendEventWorkCommon.postWork(roomId, sendWork)
.beginUniqueWork(buildWorkIdentifier(SEND_WORK), ExistingWorkPolicy.APPEND, sendWork)
.enqueue()
return CancelableWork(sendWork.id) return CancelableWork(sendWork.id)
} }


@ -80,12 +70,9 @@ internal class DefaultSendService(private val roomId: String,
} }


override fun redactEvent(event: Event, reason: String?): Cancelable { override fun redactEvent(event: Event, reason: String?): Cancelable {
//TODO manage local echo ?
//TODO manage media/attachements? //TODO manage media/attachements?
val redactWork = createRedactEventWork(event, reason) val redactWork = createRedactEventWork(event, reason)
WorkManager.getInstance() TimelineSendEventWorkCommon.postWork(roomId, redactWork)
.beginUniqueWork(buildWorkIdentifier(SEND_WORK), ExistingWorkPolicy.APPEND, redactWork)
.enqueue()
return CancelableWork(redactWork.id) return CancelableWork(redactWork.id)
} }


@ -106,14 +93,7 @@ internal class DefaultSendService(private val roomId: String,
} }


private fun saveLocalEcho(event: Event) { private fun saveLocalEcho(event: Event) {
monarchy.tryTransactionAsync { realm -> eventFactory.saveLocalEcho(monarchy, event)
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)
}
} }


private fun buildWorkIdentifier(identifier: String): String { private fun buildWorkIdentifier(identifier: String): String {
@ -124,26 +104,20 @@ internal class DefaultSendService(private val roomId: String,
val sendContentWorkerParams = SendEventWorker.Params(roomId, event) val sendContentWorkerParams = SendEventWorker.Params(roomId, event)
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)


return OneTimeWorkRequestBuilder<SendEventWorker>() return TimelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData)
.setConstraints(WORK_CONSTRAINTS)
.setInputData(sendWorkData)
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
.build()
} }


private fun createRedactEventWork(event: Event, reason: String?): OneTimeWorkRequest { 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( val sendContentWorkerParams = RedactEventWorker.Params(redactEvent.eventId!!,
roomId, event.eventId!!, reason) roomId, event.eventId, reason)
val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)


return OneTimeWorkRequestBuilder<RedactEventWorker>() return TimelineSendEventWorkCommon.createWork<RedactEventWorker>(redactWorkData)
.setConstraints(WORK_CONSTRAINTS)
.setInputData(redactWorkData)
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
.build()
} }


private fun createUploadMediaWork(event: Event, attachment: ContentAttachmentData): OneTimeWorkRequest { private fun createUploadMediaWork(event: Event, attachment: ContentAttachmentData): OneTimeWorkRequest {

View File

@ -18,21 +18,37 @@ package im.vector.matrix.android.internal.session.room.send


import android.media.MediaMetadataRetriever import android.media.MediaMetadataRetriever
import android.text.TextUtils import android.text.TextUtils
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.R import im.vector.matrix.android.R
import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.permalinks.PermalinkFactory
import im.vector.matrix.android.api.session.content.ContentAttachmentData 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.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.ReactionContent
import im.vector.matrix.android.api.session.room.model.relation.ReactionInfo 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.RelationDefaultContent
import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent 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.session.content.ThumbnailExtractor
import im.vector.matrix.android.internal.util.StringProvider import im.vector.matrix.android.internal.util.StringProvider
import im.vector.matrix.android.internal.util.tryTransactionAsync
import org.commonmark.parser.Parser import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer 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) { internal class LocalEchoEventFactory(private val credentials: Credentials, private val stringProvider: StringProvider) {


fun createTextEvent(roomId: String, msgType: String, text: String, autoMarkdown: Boolean): Event { 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 document = parser.parse(text)
val renderer = HtmlRenderer.builder().build() val renderer = HtmlRenderer.builder().build()
val htmlText = renderer.render(document) val htmlText = renderer.render(document)
if (!TextUtils.equals(text, htmlText)) { if (isFormattedTextPertinent(text, htmlText)) { //FIX that
return createFormattedTextEvent(roomId, text, htmlText) return createFormattedTextEvent(roomId, text, htmlText)
} }
} }
@ -49,6 +65,9 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva
return createEvent(roomId, content) return createEvent(roomId, content)
} }


private fun isFormattedTextPertinent(text: String, htmlText: String?) =
text != htmlText && htmlText != "<p>$text</p>\n"

fun createFormattedTextEvent(roomId: String, text: String, formattedText: String): Event { fun createFormattedTextEvent(roomId: String, text: String, formattedText: String): Event {
val content = MessageTextContent( val content = MessageTextContent(
type = MessageType.MSGTYPE_TEXT, type = MessageType.MSGTYPE_TEXT,
@ -71,7 +90,7 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva
val document = parser.parse(newBodyText) val document = parser.parse(newBodyText)
val renderer = HtmlRenderer.builder().build() val renderer = HtmlRenderer.builder().build()
val htmlText = renderer.render(document) val htmlText = renderer.render(document)
if (!TextUtils.equals(newBodyText, htmlText)) { if (isFormattedTextPertinent(newBodyText, htmlText)) {
newContent = MessageTextContent( newContent = MessageTextContent(
type = MessageType.MSGTYPE_TEXT, type = MessageType.MSGTYPE_TEXT,
format = MessageType.FORMAT_MATRIX_HTML, format = MessageType.FORMAT_MATRIX_HTML,
@ -107,14 +126,16 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva
reaction reaction
) )
) )
val localId = dummyEventId(roomId)
return Event( return Event(
roomId = roomId, roomId = roomId,
originServerTs = dummyOriginServerTs(), originServerTs = dummyOriginServerTs(),
sender = credentials.userId, sender = credentials.userId,
eventId = dummyEventId(roomId), eventId = localId,
type = EventType.REACTION, 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 { private fun createEvent(roomId: String, content: Any? = null): Event {
val localID = dummyEventId(roomId)
return Event( return Event(
roomId = roomId, roomId = roomId,
originServerTs = dummyOriginServerTs(), originServerTs = dummyOriginServerTs(),
sender = credentials.userId, sender = credentials.userId,
eventId = dummyEventId(roomId), eventId = localID,
type = EventType.MESSAGE, 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 { private fun dummyEventId(roomId: String): String {
return roomId + "-" + dummyOriginServerTs() return "m.${txNCounter++}"
} }


fun createReplyTextEvent(roomId: String, eventReplied: Event, replyText: String): Event? { 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()
}
} }

View File

@ -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.session.room.RoomAPI
import im.vector.matrix.android.internal.util.WorkerParamsFactory import im.vector.matrix.android.internal.util.WorkerParamsFactory
import org.koin.standalone.inject import org.koin.standalone.inject
import java.util.*


internal class RedactEventWorker(context: Context, params: WorkerParameters) internal class RedactEventWorker(context: Context, params: WorkerParameters)
: Worker(context, params), MatrixKoinComponent { : Worker(context, params), MatrixKoinComponent {


@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class Params( internal data class Params(
val txID: String,
val roomId: String, val roomId: String,
val eventId: String, val eventId: String,
val reason: String? val reason: String?
@ -40,26 +40,26 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters)
private val roomAPI by inject<RoomAPI>() private val roomAPI by inject<RoomAPI>()


override fun doWork(): Result { override fun doWork(): Result {
val params = WorkerParamsFactory.fromData<RedactEventWorker.Params>(inputData) val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.failure() ?: return Result.failure()


if (params.eventId == null) { val eventId = params.eventId
return Result.failure()
}
val txID = UUID.randomUUID().toString()

val result = executeRequest<SendResponse> { val result = executeRequest<SendResponse> {
apiCall = roomAPI.redactEvent( apiCall = roomAPI.redactEvent(
txID, params.txID,
params.roomId, params.roomId,
params.eventId, eventId,
if (params.reason == null) emptyMap() else mapOf("reason" to params.reason) if (params.reason == null) emptyMap() else mapOf("reason" to params.reason)
) )
} }
return result.fold({ return result.fold({
when (it) { when (it) {
is Failure.NetworkConnection -> Result.retry() 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() Result.success()

View File

@ -61,7 +61,11 @@ internal class SendEventWorker(context: Context, params: WorkerParameters)
return result.fold({ return result.fold({
when (it) { when (it) {
is Failure.NetworkConnection -> Result.retry() 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() }) }, { Result.success() })
} }

View File

@ -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 <reified W : ListenableWorker> createWork(data: Data): OneTimeWorkRequest {
return OneTimeWorkRequestBuilder<W>()
.setConstraints(WORK_CONSTRAINTS)
.setInputData(data)
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
.build()
}

private fun buildWorkIdentifier(roomId: String): String {
return "${roomId}_$SEND_WORK"
}
}

View File

@ -18,20 +18,13 @@ package im.vector.matrix.android.internal.session.room.timeline


import arrow.core.Try import arrow.core.Try
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.internal.database.helper.addAll import im.vector.matrix.android.internal.database.helper.*
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.model.ChunkEntity 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.model.RoomEntity
import im.vector.matrix.android.internal.database.query.create 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.find
import im.vector.matrix.android.internal.database.query.findAllIncludingEvents import im.vector.matrix.android.internal.database.query.findAllIncludingEvents
import im.vector.matrix.android.internal.database.query.where 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 im.vector.matrix.android.internal.util.tryTransactionSync
import io.realm.kotlin.createObject import io.realm.kotlin.createObject
import timber.log.Timber import timber.log.Timber
@ -39,8 +32,7 @@ import timber.log.Timber
/** /**
* Insert Chunk in DB, and eventually merge with existing chunk event * Insert Chunk in DB, and eventually merge with existing chunk event
*/ */
internal class TokenChunkEventPersistor(private val monarchy: Monarchy, internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {
private val eventRelationsAggregationUpdater: EventRelationsAggregationUpdater) {


/** /**
* <pre> * <pre>
@ -119,7 +111,7 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy,
Timber.v("Start persisting ${receivedChunk.events.size} events in $roomId towards $direction") Timber.v("Start persisting ${receivedChunk.events.size} events in $roomId towards $direction")


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


val nextToken: String? val nextToken: String?
val prevToken: String? val prevToken: String?
@ -142,7 +134,7 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy,
} else { } else {
nextChunk?.apply { this.prevToken = prevToken } nextChunk?.apply { this.prevToken = prevToken }
} }
?: ChunkEntity.create(realm, prevToken, nextToken) ?: ChunkEntity.create(realm, prevToken, nextToken)


if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) { if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) {
Timber.v("Reach end of $roomId") 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}") Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}")
currentChunk.addAll(roomId, receivedChunk.events, direction, isUnlinked = currentChunk.isUnlinked()) currentChunk.addAll(roomId, receivedChunk.events, direction, isUnlinked = currentChunk.isUnlinked())


//Event
eventRelationsAggregationUpdater.update(realm,roomId,receivedChunk.events.toList())
// Then we merge chunks if needed // Then we merge chunks if needed
if (currentChunk != prevChunk && prevChunk != null) { if (currentChunk != prevChunk && prevChunk != null) {
currentChunk = handleMerge(roomEntity, direction, currentChunk, prevChunk) currentChunk = handleMerge(roomEntity, direction, currentChunk, prevChunk)

View File

@ -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.find
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.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.RoomSummaryUpdater
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection 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.*
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 io.realm.Realm import io.realm.Realm
import io.realm.kotlin.createObject import io.realm.kotlin.createObject
import timber.log.Timber import timber.log.Timber
@ -46,8 +41,7 @@ import timber.log.Timber
internal class RoomSyncHandler(private val monarchy: Monarchy, internal class RoomSyncHandler(private val monarchy: Monarchy,
private val readReceiptHandler: ReadReceiptHandler, private val readReceiptHandler: ReadReceiptHandler,
private val roomSummaryUpdater: RoomSummaryUpdater, private val roomSummaryUpdater: RoomSummaryUpdater,
private val roomTagHandler: RoomTagHandler, private val roomTagHandler: RoomTagHandler) {
private val eventRelationsAggregationUpdater: EventRelationsAggregationUpdater) {


sealed class HandlingStrategy { sealed class HandlingStrategy {
data class JOINED(val data: Map<String, RoomSync>) : HandlingStrategy() data class JOINED(val data: Map<String, RoomSync>) : HandlingStrategy()
@ -67,9 +61,9 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,


private fun handleRoomSync(realm: Realm, handlingStrategy: HandlingStrategy) { private fun handleRoomSync(realm: Realm, handlingStrategy: HandlingStrategy) {
val rooms = when (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.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) realm.insertOrUpdate(rooms)
} }
@ -81,7 +75,7 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
Timber.v("Handle join sync for room $roomId") Timber.v("Handle join sync for room $roomId")


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


if (roomEntity.membership == Membership.INVITE) { if (roomEntity.membership == Membership.INVITE) {
roomEntity.chunks.deleteAllFromRealm() roomEntity.chunks.deleteAllFromRealm()
@ -116,11 +110,13 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
transactionIds.forEach { transactionIds.forEach {
val sendingEventEntity = roomEntity.sendingTimelineEvents.find(it) val sendingEventEntity = roomEntity.sendingTimelineEvents.find(it)
if (sendingEventEntity != null) { if (sendingEventEntity != null) {
Timber.v("Remove local echo for tx:$it")
roomEntity.sendingTimelineEvents.remove(sendingEventEntity) 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) roomSummaryUpdater.update(realm, roomId, Membership.JOIN, roomSync.summary, roomSync.unreadNotifications)


if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) { if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) {
@ -139,7 +135,7 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
InvitedRoomSync): RoomEntity { InvitedRoomSync): RoomEntity {
Timber.v("Handle invited sync for room $roomId") Timber.v("Handle invited sync for room $roomId")
val roomEntity = RoomEntity.where(realm, roomId).findFirst() val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId) ?: realm.createObject(roomId)
roomEntity.membership = Membership.INVITE roomEntity.membership = Membership.INVITE
if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) { if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) {
val chunkEntity = handleTimelineEvents(realm, roomId, roomSync.inviteState.events) val chunkEntity = handleTimelineEvents(realm, roomId, roomSync.inviteState.events)
@ -153,7 +149,7 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
roomId: String, roomId: String,
roomSync: RoomSync): RoomEntity { roomSync: RoomSync): RoomEntity {
val roomEntity = RoomEntity.where(realm, roomId).findFirst() val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId) ?: realm.createObject(roomId)


roomEntity.membership = Membership.LEAVE roomEntity.membership = Membership.LEAVE
roomEntity.chunks.deleteAllFromRealm() roomEntity.chunks.deleteAllFromRealm()

View File

@ -40,7 +40,7 @@ internal class SyncModule {
} }


scope(DefaultSession.SCOPE) { scope(DefaultSession.SCOPE) {
RoomSyncHandler(get(), get(), get(), get(), get()) RoomSyncHandler(get(), get(), get(), get())
} }


scope(DefaultSession.SCOPE) { scope(DefaultSession.SCOPE) {

View File

@ -90,7 +90,9 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
avatarUrl = avatarUrl, avatarUrl = avatarUrl,
memberName = formattedMemberName, memberName = formattedMemberName,
showInformation = showInformation, 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 hasBeenEdited = hasBeenEdited
) )



View File

@ -112,11 +112,12 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
(holder.reactionWrapper?.children?.elementAtOrNull(index) as? ReactionButton)?.let { reactionButton -> (holder.reactionWrapper?.children?.elementAtOrNull(index) as? ReactionButton)?.let { reactionButton ->
reactionButton.isVisible = true reactionButton.isVisible = true
reactionButton.reactedListener = reactionClickListener reactionButton.reactedListener = reactionClickListener
reactionButton.setTag(R.id.messageBottomInfo, reaction.first) reactionButton.setTag(R.id.messageBottomInfo, reaction.key)
idToRefInFlow.add(reactionButton.id) idToRefInFlow.add(reactionButton.id)
reactionButton.reactionString = reaction.first reactionButton.reactionString = reaction.key
reactionButton.reactionCount = reaction.second reactionButton.reactionCount = reaction.count
reactionButton.setChecked(reaction.third) 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), // Just setting the view as gone will break the FlowHelper (and invisible will take too much space),

View File

@ -16,9 +16,8 @@


package im.vector.riotredesign.features.home.room.detail.timeline.item 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 android.os.Parcelable
import im.vector.matrix.android.api.session.room.send.SendState
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize


@Parcelize @Parcelize
@ -31,6 +30,15 @@ data class MessageInformationData(
val memberName: CharSequence? = null, val memberName: CharSequence? = null,
val showInformation: Boolean = true, val showInformation: Boolean = true,
/*List of reactions (emoji,count,isSelected)*/ /*List of reactions (emoji,count,isSelected)*/
var orderedReactionList: List<Triple<String,Int,Boolean>>? = null, var orderedReactionList: List<ReactionInfoData>? = null,
var hasBeenEdited: Boolean = false var hasBeenEdited: Boolean = false
) : Parcelable ) : Parcelable


@Parcelize
data class ReactionInfoData(
val key: String,
val count: Int,
val addedByMe: Boolean,
val synced: Boolean
) : Parcelable