Merge pull request #2940 from vector-im/feature/bma/various_fixies

Rework event edition management
This commit is contained in:
Benoit Marty 2021-03-04 18:03:45 +01:00 committed by GitHub
commit e5656e264a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 317 additions and 281 deletions

View file

@ -30,7 +30,7 @@ Test:
-
Other changes:
-
- Rework edition of event management
Changes in Element 1.1.0 (2021-02-19)
===================================================

View file

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/design_default_color_primary">
<TextView
android:id="@+id/testPage"
android:layout_centerInParent="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="1"
android:textSize="80sp"
android:textStyle="bold" />
</RelativeLayout>

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.crypto
enum class VerificationState {
REQUEST,
WAITING,
CANCELED_BY_ME,
CANCELED_BY_OTHER,
DONE
}
fun VerificationState.isCanceled(): Boolean {
return this == VerificationState.CANCELED_BY_ME || this == VerificationState.CANCELED_BY_OTHER
}

View file

@ -18,7 +18,7 @@ package org.matrix.android.sdk.api.session.room.model
import org.matrix.android.sdk.api.session.events.model.Content
data class EditAggregatedSummary(
val aggregatedContent: Content? = null,
val latestContent: 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<String>,
val localEchos: List<String>,

View file

@ -17,7 +17,7 @@ package org.matrix.android.sdk.api.session.room.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.internal.session.room.VerificationState
import org.matrix.android.sdk.api.crypto.VerificationState
/**
* Contains an aggregated summary info of the references.

View file

@ -16,7 +16,6 @@
package org.matrix.android.sdk.api.session.room.model.relation
import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
@ -92,8 +91,11 @@ interface RelationService {
/**
* Get the edit history of the given event
* The return list will contain the original event and all the editions of this event, done by the
* same sender, sorted in the reverse order (so the original event is the latest element, and the
* latest edition is the first element of the list)
*/
fun fetchEditHistory(eventId: String, callback: MatrixCallback<List<Event>>)
suspend fun fetchEditHistory(eventId: String): List<Event>
/**
* Reply to an event in the timeline (must be in same room)

View file

@ -123,8 +123,7 @@ fun TimelineEvent.getLastMessageContent(): MessageContent? {
return if (root.getClearType() == EventType.STICKER) {
root.getClearContent().toModel<MessageStickerContent>()
} else {
annotations?.editSummary?.aggregatedContent?.toModel()
?: root.getClearContent().toModel()
(annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel()
}
}

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.verification
import org.matrix.android.sdk.api.crypto.VerificationState
import org.matrix.android.sdk.api.crypto.isCanceled
// State transition with control
internal fun VerificationState?.toState(newState: VerificationState): VerificationState {
// Cancel is always prioritary ?
// Eg id i found that mac or keys mismatch and send a cancel and the other send a done, i have to
// consider as canceled
if (newState.isCanceled()) {
return newState
}
// never move out of cancel
if (this?.isCanceled() == true) {
return this
}
return newState
}

View file

@ -18,6 +18,8 @@ package org.matrix.android.sdk.internal.database
import io.realm.DynamicRealm
import io.realm.RealmMigration
import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.EditionOfEventFields
import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields
import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntityFields
@ -30,7 +32,7 @@ import javax.inject.Inject
class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
companion object {
const val SESSION_STORE_SCHEMA_VERSION = 7L
const val SESSION_STORE_SCHEMA_VERSION = 8L
}
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
@ -43,6 +45,7 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
if (oldVersion <= 4) migrateTo5(realm)
if (oldVersion <= 5) migrateTo6(realm)
if (oldVersion <= 6) migrateTo7(realm)
if (oldVersion <= 7) migrateTo8(realm)
}
private fun migrateTo1(realm: DynamicRealm) {
@ -122,4 +125,28 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
}
?.removeField("areAllMembersLoaded")
}
private fun migrateTo8(realm: DynamicRealm) {
Timber.d("Step 7 -> 8")
val editionOfEventSchema = realm.schema.create("EditionOfEvent")
.apply {
// setEmbedded does not return `this`...
isEmbedded = true
}
.addField(EditionOfEventFields.CONTENT, String::class.java)
.addField(EditionOfEventFields.EVENT_ID, String::class.java)
.setRequired(EditionOfEventFields.EVENT_ID, true)
.addField(EditionOfEventFields.SENDER_ID, String::class.java)
.setRequired(EditionOfEventFields.SENDER_ID, true)
.addField(EditionOfEventFields.TIMESTAMP, Long::class.java)
.addField(EditionOfEventFields.IS_LOCAL_ECHO, Boolean::class.java)
realm.schema.get("EditAggregatedSummaryEntity")
?.removeField("aggregatedContent")
?.removeField("sourceEvents")
?.removeField("lastEditTs")
?.removeField("sourceLocalEchoEvents")
?.addRealmListField(EditAggregatedSummaryEntityFields.EDITIONS.`$`, editionOfEventSchema)
}
}

View file

@ -97,7 +97,8 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String,
this.root = eventEntity
this.eventId = eventId
this.roomId = roomId
this.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
this.annotations = EventAnnotationsSummaryEntity.where(realm, roomId, eventId).findFirst()
?.also { it.cleanUp(eventEntity.sender) }
this.readReceipts = readReceiptsSummaryEntity
this.displayIndex = displayIndex
val roomMemberContent = roomMemberContentsByUser[senderId]

View file

@ -20,11 +20,7 @@ import org.matrix.android.sdk.api.session.room.model.EditAggregatedSummary
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
import org.matrix.android.sdk.api.session.room.model.ReactionAggregatedSummary
import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedSummary
import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntity
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryEntity
import org.matrix.android.sdk.internal.database.model.ReferencesAggregatedSummaryEntity
import io.realm.RealmList
internal object EventAnnotationsSummaryMapper {
fun map(annotationsSummary: EventAnnotationsSummaryEntity): EventAnnotationsSummary {
@ -40,14 +36,18 @@ internal object EventAnnotationsSummaryMapper {
it.sourceLocalEcho.toList()
)
},
editSummary = annotationsSummary.editSummary?.let {
EditAggregatedSummary(
ContentMapper.map(it.aggregatedContent),
it.sourceEvents.toList(),
it.sourceLocalEchoEvents.toList(),
it.lastEditTs
)
},
editSummary = annotationsSummary.editSummary
?.let {
val latestEdition = it.editions.maxByOrNull { editionOfEvent -> editionOfEvent.timestamp } ?: return@let null
EditAggregatedSummary(
latestContent = ContentMapper.map(latestEdition.content),
sourceEvents = it.editions.filter { editionOfEvent -> !editionOfEvent.isLocalEcho }
.map { editionOfEvent -> editionOfEvent.eventId },
localEchos = it.editions.filter { editionOfEvent -> editionOfEvent.isLocalEcho }
.map { editionOfEvent -> editionOfEvent.eventId },
lastEditTs = latestEdition.timestamp
)
},
referencesAggregatedSummary = annotationsSummary.referencesSummaryEntity?.let {
ReferencesAggregatedSummary(
it.eventId,
@ -62,46 +62,6 @@ internal object EventAnnotationsSummaryMapper {
)
}
fun map(annotationsSummary: EventAnnotationsSummary, roomId: String): EventAnnotationsSummaryEntity {
val eventAnnotationsSummaryEntity = EventAnnotationsSummaryEntity()
eventAnnotationsSummaryEntity.eventId = annotationsSummary.eventId
eventAnnotationsSummaryEntity.roomId = roomId
eventAnnotationsSummaryEntity.editSummary = annotationsSummary.editSummary?.let {
EditAggregatedSummaryEntity(
ContentMapper.map(it.aggregatedContent),
RealmList<String>().apply { addAll(it.sourceEvents) },
RealmList<String>().apply { addAll(it.localEchos) },
it.lastEditTs
)
}
eventAnnotationsSummaryEntity.reactionsSummary = annotationsSummary.reactionsSummary.let {
RealmList<ReactionAggregatedSummaryEntity>().apply {
addAll(it.map {
ReactionAggregatedSummaryEntity(
it.key,
it.count,
it.addedByMe,
it.firstTimestamp,
RealmList<String>().apply { addAll(it.sourceEvents) },
RealmList<String>().apply { addAll(it.localEchoEvents) }
)
})
}
}
eventAnnotationsSummaryEntity.referencesSummaryEntity = annotationsSummary.referencesAggregatedSummary?.let {
ReferencesAggregatedSummaryEntity(
it.eventId,
ContentMapper.map(it.content),
RealmList<String>().apply { addAll(it.sourceEvents) },
RealmList<String>().apply { addAll(it.localEchos) }
)
}
eventAnnotationsSummaryEntity.pollResponseSummary = annotationsSummary.pollResponseSummary?.let {
PollResponseAggregatedSummaryEntityMapper.map(it)
}
return eventAnnotationsSummaryEntity
}
}
internal fun EventAnnotationsSummaryEntity.asDomain(): EventAnnotationsSummary {

View file

@ -17,17 +17,24 @@ package org.matrix.android.sdk.internal.database.model
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.annotations.RealmClass
/**
* Keep the latest state of edition of a message
* Keep all the editions of a message
*/
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<String> = RealmList(),
var sourceLocalEchoEvents: RealmList<String> = RealmList(),
var lastEditTs: Long = 0
// The list of the editions used to build the summary (might be out of sync if chunked received from message chunk)
var editions: RealmList<EditionOfEvent> = RealmList()
) : RealmObject() {
companion object
}
@RealmClass(embedded = true)
internal open class EditionOfEvent(
var senderId: String = "",
var eventId: String = "",
var content: String? = null,
var timestamp: Long = 0,
var isLocalEcho: Boolean = false
) : RealmObject()

View file

@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.database.model
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
import timber.log.Timber
internal open class EventAnnotationsSummaryEntity(
@PrimaryKey
@ -29,6 +30,21 @@ internal open class EventAnnotationsSummaryEntity(
var pollResponseSummary: PollResponseAggregatedSummaryEntity? = null
) : RealmObject() {
/**
* Cleanup undesired editions, done by users different from the originalEventSender
*/
fun cleanUp(originalEventSenderId: String?) {
originalEventSenderId ?: return
editSummary?.editions?.filter {
it.senderId != originalEventSenderId
}
?.forEach {
Timber.w("Deleting an edition from ${it.senderId} of event sent by $originalEventSenderId")
it.deleteFromRealm()
}
}
companion object
}

View file

@ -43,6 +43,7 @@ import io.realm.annotations.RealmModule
EventAnnotationsSummaryEntity::class,
ReactionAggregatedSummaryEntity::class,
EditAggregatedSummaryEntity::class,
EditionOfEvent::class,
PollResponseAggregatedSummaryEntity::class,
ReferencesAggregatedSummaryEntity::class,
PushRulesEntity::class,

View file

@ -23,18 +23,10 @@ import io.realm.Realm
import io.realm.RealmQuery
import io.realm.kotlin.where
internal fun EventAnnotationsSummaryEntity.Companion.where(realm: Realm, eventId: String): RealmQuery<EventAnnotationsSummaryEntity> {
val query = realm.where<EventAnnotationsSummaryEntity>()
query.equalTo(EventAnnotationsSummaryEntityFields.EVENT_ID, eventId)
return query
}
internal fun EventAnnotationsSummaryEntity.Companion.whereInRoom(realm: Realm, roomId: String?): RealmQuery<EventAnnotationsSummaryEntity> {
val query = realm.where<EventAnnotationsSummaryEntity>()
if (roomId != null) {
query.equalTo(EventAnnotationsSummaryEntityFields.ROOM_ID, roomId)
}
return query
internal fun EventAnnotationsSummaryEntity.Companion.where(realm: Realm, roomId: String, eventId: String): RealmQuery<EventAnnotationsSummaryEntity> {
return realm.where<EventAnnotationsSummaryEntity>()
.equalTo(EventAnnotationsSummaryEntityFields.ROOM_ID, roomId)
.equalTo(EventAnnotationsSummaryEntityFields.EVENT_ID, eventId)
}
internal fun EventAnnotationsSummaryEntity.Companion.create(realm: Realm, roomId: String, eventId: String): EventAnnotationsSummaryEntity {
@ -49,6 +41,6 @@ internal fun EventAnnotationsSummaryEntity.Companion.create(realm: Realm, roomId
}
internal fun EventAnnotationsSummaryEntity.Companion.getOrCreate(realm: Realm, roomId: String, eventId: String): EventAnnotationsSummaryEntity {
return EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
?: EventAnnotationsSummaryEntity.create(realm, roomId, eventId).apply { this.roomId = roomId }
return EventAnnotationsSummaryEntity.where(realm, roomId, eventId).findFirst()
?: EventAnnotationsSummaryEntity.create(realm, roomId, eventId)
}

View file

@ -16,6 +16,7 @@
package org.matrix.android.sdk.internal.session.room
import io.realm.Realm
import org.matrix.android.sdk.api.crypto.VerificationState
import org.matrix.android.sdk.api.session.events.model.AggregatedAnnotation
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
@ -31,9 +32,11 @@ import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponse
import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent
import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
import org.matrix.android.sdk.internal.crypto.verification.toState
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
import org.matrix.android.sdk.internal.database.mapper.EventMapper
import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntity
import org.matrix.android.sdk.internal.database.model.EditionOfEvent
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.EventInsertType
@ -50,33 +53,6 @@ import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
import timber.log.Timber
import javax.inject.Inject
enum class VerificationState {
REQUEST,
WAITING,
CANCELED_BY_ME,
CANCELED_BY_OTHER,
DONE
}
fun VerificationState.isCanceled(): Boolean {
return this == VerificationState.CANCELED_BY_ME || this == VerificationState.CANCELED_BY_OTHER
}
// State transition with control
private fun VerificationState?.toState(newState: VerificationState): VerificationState {
// Cancel is always prioritary ?
// Eg id i found that mac or keys mismatch and send a cancel and the other send a done, i have to
// consider as canceled
if (newState.isCanceled()) {
return newState
}
// never move out of cancel
if (this?.isCanceled() == true) {
return this
}
return newState
}
internal class EventRelationsAggregationProcessor @Inject constructor(@UserId private val userId: String)
: EventInsertLiveProcessor {
@ -118,13 +94,11 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr
Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}")
handleInitialAggregatedRelations(event, roomId, event.unsignedData.relations.annotations, realm)
EventAnnotationsSummaryEntity.where(realm, event.eventId
?: "").findFirst()?.let {
TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId
?: "").findFirst()?.let { tet ->
tet.annotations = it
}
}
EventAnnotationsSummaryEntity.where(realm, roomId, event.eventId ?: "").findFirst()
?.let {
TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId ?: "").findFirst()
?.let { tet -> tet.annotations = it }
}
}
val content: MessageContent? = event.content.toModel()
@ -216,63 +190,78 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr
// OPT OUT serer aggregation until API mature enough
private val SHOULD_HANDLE_SERVER_AGREGGATION = false
private fun handleReplace(realm: Realm, event: Event, content: MessageContent, roomId: String, isLocalEcho: Boolean, relatedEventId: String? = null) {
private fun handleReplace(realm: Realm,
event: Event,
content: MessageContent,
roomId: String,
isLocalEcho: Boolean,
relatedEventId: String? = null) {
val eventId = event.eventId ?: return
val targetEventId = relatedEventId ?: content.relatesTo?.eventId ?: return
val newContent = content.newContent ?: return
// Check that the sender is the same
val editedEvent = EventEntity.where(realm, targetEventId).findFirst()
if (editedEvent == null) {
// We do not know yet about the edited event
} else if (editedEvent.sender != event.senderId) {
// Edited by someone else, ignore
Timber.w("Ignore edition by someone else")
return
}
// ok, this is a replace
val existing = EventAnnotationsSummaryEntity.getOrCreate(realm, roomId, targetEventId)
val eventAnnotationsSummaryEntity = EventAnnotationsSummaryEntity.getOrCreate(realm, roomId, targetEventId)
// we have it
val existingSummary = existing.editSummary
val existingSummary = eventAnnotationsSummaryEntity.editSummary
if (existingSummary == null) {
Timber.v("###REPLACE new edit summary for $targetEventId, creating one (localEcho:$isLocalEcho)")
// create the edit summary
val editSummary = realm.createObject(EditAggregatedSummaryEntity::class.java)
editSummary.aggregatedContent = ContentMapper.map(newContent)
if (isLocalEcho) {
editSummary.lastEditTs = 0
editSummary.sourceLocalEchoEvents.add(eventId)
} else {
editSummary.lastEditTs = event.originServerTs ?: 0
editSummary.sourceEvents.add(eventId)
}
existing.editSummary = editSummary
eventAnnotationsSummaryEntity.editSummary = realm.createObject(EditAggregatedSummaryEntity::class.java)
.also { editSummary ->
editSummary.editions.add(
EditionOfEvent(
senderId = event.senderId ?: "",
eventId = event.eventId,
content = ContentMapper.map(newContent),
timestamp = if (isLocalEcho) 0 else event.originServerTs ?: 0,
isLocalEcho = isLocalEcho
)
)
}
} else {
if (existingSummary.sourceEvents.contains(eventId)) {
if (existingSummary.editions.any { it.eventId == 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)) {
if (!isLocalEcho && existingSummary.editions.any { it.eventId == 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 (
isLocalEcho // do not rely on ts for local echo, take it
|| event.originServerTs ?: 0 >= existingSummary.lastEditTs
) {
Timber.v("###REPLACE Computing aggregated edit summary (isLocalEcho:$isLocalEcho)")
if (!isLocalEcho) {
// Do not take local echo originServerTs here, could mess up ordering (keep old ts)
existingSummary.lastEditTs = event.originServerTs ?: System.currentTimeMillis()
}
existingSummary.aggregatedContent = ContentMapper.map(newContent)
if (isLocalEcho) {
existingSummary.sourceLocalEchoEvents.add(eventId)
} else {
existingSummary.sourceEvents.add(eventId)
existingSummary.editions.firstOrNull { it.eventId == txId }?.let {
it.eventId = event.eventId
it.timestamp = event.originServerTs ?: System.currentTimeMillis()
it.isLocalEcho = false
}
} else {
// ignore this event for the summary (back paginate)
if (!isLocalEcho) {
existingSummary.sourceEvents.add(eventId)
}
Timber.v("###REPLACE ignoring event for summary, it's to old $eventId")
Timber.v("###REPLACE Computing aggregated edit summary (isLocalEcho:$isLocalEcho)")
existingSummary.editions.add(
EditionOfEvent(
senderId = event.senderId ?: "",
eventId = event.eventId,
content = ContentMapper.map(newContent),
timestamp = if (isLocalEcho) {
System.currentTimeMillis()
} else {
// Do not take local echo originServerTs here, could mess up ordering (keep old ts)
event.originServerTs ?: System.currentTimeMillis()
},
isLocalEcho = isLocalEcho
)
)
}
}
}
@ -290,7 +279,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr
val eventTimestamp = event.originServerTs ?: return
// ok, this is a poll response
var existing = EventAnnotationsSummaryEntity.where(realm, targetEventId).findFirst()
var existing = EventAnnotationsSummaryEntity.where(realm, roomId, targetEventId).findFirst()
if (existing == null) {
Timber.v("## POLL creating new relation summary for $targetEventId")
existing = EventAnnotationsSummaryEntity.create(realm, roomId, targetEventId)
@ -370,7 +359,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr
aggregation.chunk?.forEach {
if (it.type == EventType.REACTION) {
val eventId = event.eventId ?: ""
val existing = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
val existing = EventAnnotationsSummaryEntity.where(realm, roomId, eventId).findFirst()
if (existing == null) {
val eventSummary = EventAnnotationsSummaryEntity.create(realm, roomId, eventId)
val sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java)
@ -454,46 +443,29 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr
*/
private fun handleRedactionOfReplace(redacted: EventEntity, relatedEventId: String, realm: Realm) {
Timber.d("Handle redaction of m.replace")
val eventSummary = EventAnnotationsSummaryEntity.where(realm, relatedEventId).findFirst()
val eventSummary = EventAnnotationsSummaryEntity.where(realm, redacted.roomId, 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)
val sourceToDiscard = eventSummary.editSummary?.editions?.firstOrNull { it.eventId == 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
}
}
// Need to remove this event from the edition list
sourceToDiscard.deleteFromRealm()
}
fun handleReactionRedact(eventToPrune: EventEntity, realm: Realm, userId: String) {
private fun handleReactionRedact(eventToPrune: EventEntity, realm: Realm, userId: String) {
Timber.v("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 reactionContent: ReactionContent = EventMapper.map(eventToPrune).content.toModel() ?: return
val eventThatWasReacted = reactionContent.relatesTo?.eventId ?: return
val reactionKey = reactionContent.relatesTo.key
Timber.v("REMOVE reaction for key $reactionKey")
val summary = EventAnnotationsSummaryEntity.where(realm, eventThatWasReacted).findFirst()
val summary = EventAnnotationsSummaryEntity.where(realm, eventToPrune.roomId, eventThatWasReacted).findFirst()
if (summary != null) {
summary.reactionsSummary.where()
.equalTo(ReactionAggregatedSummaryEntityFields.KEY, reactionKey)

View file

@ -140,13 +140,8 @@ internal class DefaultRelationService @AssistedInject constructor(
return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
}
override fun fetchEditHistory(eventId: String, callback: MatrixCallback<List<Event>>) {
val params = FetchEditHistoryTask.Params(roomId, eventId)
fetchEditHistoryTask
.configureWith(params) {
this.callback = callback
}
.executeBy(taskExecutor)
override suspend fun fetchEditHistory(eventId: String): List<Event> {
return fetchEditHistoryTask.execute(FetchEditHistoryTask.Params(roomId, eventId))
}
override fun replyToMessage(eventReplied: TimelineEvent, replyText: CharSequence, autoMarkdown: Boolean): Cancelable? {
@ -159,7 +154,7 @@ internal class DefaultRelationService @AssistedInject constructor(
override fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary? {
return monarchy.fetchCopyMap(
{ EventAnnotationsSummaryEntity.where(it, eventId).findFirst() },
{ EventAnnotationsSummaryEntity.where(it, roomId, eventId).findFirst() },
{ entity, _ ->
entity.asDomain()
}
@ -168,7 +163,7 @@ internal class DefaultRelationService @AssistedInject constructor(
override fun getEventAnnotationsSummaryLive(eventId: String): LiveData<Optional<EventAnnotationsSummary>> {
val liveData = monarchy.findAllMappedWithChanges(
{ EventAnnotationsSummaryEntity.where(it, eventId) },
{ EventAnnotationsSummaryEntity.where(it, roomId, eventId) },
{ it.asDomain() }
)
return Transformations.map(liveData) { results ->

View file

@ -49,8 +49,11 @@ internal class DefaultFetchEditHistoryTask @Inject constructor(
)
}
val events = response.chunks.toMutableList()
response.originalEvent?.let { events.add(it) }
return events
// Filter out edition form other users, and redacted editions
val originalSenderId = response.originalEvent?.senderId
val events = response.chunks
.filter { it.senderId == originalSenderId }
.filter { !it.isRedacted() }
return events + listOfNotNull(response.originalEvent)
}
}

View file

@ -45,16 +45,16 @@ internal class DefaultFindReactionEventForUndoTask @Inject constructor(
override suspend fun execute(params: FindReactionEventForUndoTask.Params): FindReactionEventForUndoTask.Result {
val eventId = Realm.getInstance(monarchy.realmConfiguration).use { realm ->
getReactionToRedact(realm, params.reaction, params.eventId)?.eventId
getReactionToRedact(realm, params)?.eventId
}
return FindReactionEventForUndoTask.Result(eventId)
}
private fun getReactionToRedact(realm: Realm, reaction: String, eventId: String): EventEntity? {
val summary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() ?: return null
private fun getReactionToRedact(realm: Realm, params: FindReactionEventForUndoTask.Params): EventEntity? {
val summary = EventAnnotationsSummaryEntity.where(realm, params.roomId, params.eventId).findFirst() ?: return null
val rase = summary.reactionsSummary.where()
.equalTo(ReactionAggregatedSummaryEntityFields.KEY, reaction)
.equalTo(ReactionAggregatedSummaryEntityFields.KEY, params.reaction)
.findFirst() ?: return null
// want to find the event originated by me!

View file

@ -47,22 +47,22 @@ internal class DefaultUpdateQuickReactionTask @Inject constructor(@SessionDataba
override suspend fun execute(params: UpdateQuickReactionTask.Params): UpdateQuickReactionTask.Result {
var res: Pair<String?, List<String>?>? = null
monarchy.doWithRealm { realm ->
res = updateQuickReaction(realm, params.reaction, params.oppositeReaction, params.eventId)
res = updateQuickReaction(realm, params)
}
return UpdateQuickReactionTask.Result(res?.first, res?.second.orEmpty())
}
private fun updateQuickReaction(realm: Realm, reaction: String, oppositeReaction: String, eventId: String): Pair<String?, List<String>?> {
private fun updateQuickReaction(realm: Realm, params: UpdateQuickReactionTask.Params): Pair<String?, List<String>?> {
// the emoji reaction has been selected, we need to check if we have reacted it or not
val existingSummary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
?: return Pair(reaction, null)
val existingSummary = EventAnnotationsSummaryEntity.where(realm, params.roomId, params.eventId).findFirst()
?: return Pair(params.reaction, null)
// Ok there is already reactions on this event, have we reacted to it
val aggregationForReaction = existingSummary.reactionsSummary.where()
.equalTo(ReactionAggregatedSummaryEntityFields.KEY, reaction)
.equalTo(ReactionAggregatedSummaryEntityFields.KEY, params.reaction)
.findFirst()
val aggregationForOppositeReaction = existingSummary.reactionsSummary.where()
.equalTo(ReactionAggregatedSummaryEntityFields.KEY, oppositeReaction)
.equalTo(ReactionAggregatedSummaryEntityFields.KEY, params.oppositeReaction)
.findFirst()
if (aggregationForReaction == null || !aggregationForReaction.addedByMe) {
@ -72,7 +72,7 @@ internal class DefaultUpdateQuickReactionTask @Inject constructor(@SessionDataba
val entity = EventEntity.where(realm, it).findFirst()
if (entity?.sender == userId) entity.eventId else null
}
return Pair(reaction, toRedact)
return Pair(params.reaction, toRedact)
} else {
// I already added it, so i need to undo it (like a toggle)
// find all m.redaction coming from me to readact them

View file

@ -85,7 +85,6 @@ import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.model.message.OptionItem
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
@ -95,6 +94,7 @@ import org.matrix.android.sdk.api.session.room.read.ReadService
import org.matrix.android.sdk.api.session.room.send.UserDraft
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
import org.matrix.android.sdk.api.session.room.timeline.getRelationContent
import org.matrix.android.sdk.api.session.room.timeline.getTextEditableContent
import org.matrix.android.sdk.api.session.widgets.model.Widget
@ -825,9 +825,7 @@ class RoomDetailViewModel @AssistedInject constructor(
room.editReply(state.sendMode.timelineEvent, it, action.text.toString())
}
} else {
val messageContent: MessageContent? =
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
val messageContent = state.sendMode.timelineEvent.getLastMessageContent()
val existingBody = messageContent?.body ?: ""
if (existingBody != action.text) {
room.editTextMessage(state.sendMode.timelineEvent.root.eventId ?: "",
@ -842,9 +840,7 @@ class RoomDetailViewModel @AssistedInject constructor(
popDraft()
}
is SendMode.QUOTE -> {
val messageContent: MessageContent? =
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
val messageContent = state.sendMode.timelineEvent.getLastMessageContent()
val textMsg = messageContent?.body
val finalText = legacyRiotQuoteText(textMsg, action.text.toString())

View file

@ -229,8 +229,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
}
private fun actionsForEvent(timelineEvent: TimelineEvent, actionPermissions: ActionPermissions): List<EventSharedAction> {
val messageContent: MessageContent? = timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
?: timelineEvent.root.getClearContent().toModel()
val messageContent = timelineEvent.getLastMessageContent()
val msgType = messageContent?.msgType
return arrayListOf<EventSharedAction>().apply {

View file

@ -15,42 +15,28 @@
*/
package im.vector.app.features.home.room.detail.timeline.edithistory
import com.airbnb.mvrx.Async
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.platform.EmptyAction
import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.home.room.detail.timeline.action.TimelineEventFragmentArgs
import org.matrix.android.sdk.api.MatrixCallback
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.isReply
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
import timber.log.Timber
import java.util.UUID
data class ViewEditHistoryViewState(
val eventId: String,
val roomId: String,
val isOriginalAReply: Boolean = false,
val editList: Async<List<Event>> = Uninitialized)
: MvRxState {
constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId)
}
class ViewEditHistoryViewModel @AssistedInject constructor(@Assisted
initialState: ViewEditHistoryViewState,
val session: Session,
@ -82,48 +68,48 @@ class ViewEditHistoryViewModel @AssistedInject constructor(@Assisted
private fun loadHistory() {
setState { copy(editList = Loading()) }
room.fetchEditHistory(eventId, object : MatrixCallback<List<Event>> {
override fun onFailure(failure: Throwable) {
viewModelScope.launch {
val data = try {
room.fetchEditHistory(eventId)
} catch (failure: Throwable) {
setState {
copy(editList = Fail(failure))
}
return@launch
}
override fun onSuccess(data: List<Event>) {
var originalIsReply = false
var originalIsReply = false
val events = data.map { event ->
val timelineID = event.roomId + UUID.randomUUID().toString()
event.also {
// We need to check encryption
if (it.isEncrypted() && it.mxDecryptionResult == null) {
// for now decrypt sync
try {
val result = session.cryptoService().decryptEvent(it, timelineID)
it.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
} catch (e: MXCryptoError) {
Timber.w("Failed to decrypt event in history")
}
}
if (event.eventId == it.eventId) {
originalIsReply = it.isReply()
}
data.forEach { event ->
val timelineID = event.roomId + UUID.randomUUID().toString()
// We need to check encryption
if (event.isEncrypted() && event.mxDecryptionResult == null) {
// for now decrypt sync
try {
val result = session.cryptoService().decryptEvent(event, timelineID)
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
} catch (e: MXCryptoError) {
Timber.w("Failed to decrypt event in history")
}
}
setState {
copy(
editList = Success(events),
isOriginalAReply = originalIsReply
)
if (event.eventId == eventId) {
originalIsReply = event.isReply()
}
}
})
setState {
copy(
editList = Success(data),
isOriginalAReply = originalIsReply
)
}
}
}
override fun handle(action: EmptyAction) {

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2021 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.app.features.home.room.detail.timeline.edithistory
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.app.features.home.room.detail.timeline.action.TimelineEventFragmentArgs
import org.matrix.android.sdk.api.session.events.model.Event
data class ViewEditHistoryViewState(
val eventId: String,
val roomId: String,
val isOriginalAReply: Boolean = false,
val editList: Async<List<Event>> = Uninitialized)
: MvRxState {
constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId)
}

View file

@ -26,6 +26,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.MessageInformatio
import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
import im.vector.app.features.home.room.detail.timeline.item.StatusTileTimelineItem
import im.vector.app.features.home.room.detail.timeline.item.StatusTileTimelineItem_
import org.matrix.android.sdk.api.crypto.VerificationState
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
import org.matrix.android.sdk.api.session.crypto.verification.safeValueOf
@ -35,7 +36,6 @@ import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationCancelContent
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.internal.session.room.VerificationState
import javax.inject.Inject
/**

View file

@ -26,6 +26,7 @@ import im.vector.app.features.home.room.detail.timeline.item.ReactionInfoData
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.app.features.home.room.detail.timeline.item.ReferencesInfoData
import im.vector.app.features.settings.VectorPreferences
import org.matrix.android.sdk.api.crypto.VerificationState
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.EventType
@ -37,7 +38,6 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
import org.matrix.android.sdk.api.session.room.timeline.hasBeenEdited
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
import org.matrix.android.sdk.internal.session.room.VerificationState
import javax.inject.Inject
/**

View file

@ -18,9 +18,9 @@ package im.vector.app.features.home.room.detail.timeline.item
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.crypto.VerificationState
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.internal.session.room.VerificationState
@Parcelize
data class MessageInformationData(

View file

@ -35,8 +35,8 @@ import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.RoomDetailAction
import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import org.matrix.android.sdk.api.crypto.VerificationState
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
import org.matrix.android.sdk.internal.session.room.VerificationState
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_state)
abstract class VerificationRequestItem : AbsBaseMessageItem<VerificationRequestItem.Holder>() {

View file

@ -48,6 +48,7 @@ class HomeserverSettingsController @Inject constructor(
data ?: return
buildHeader(data)
buildCapabilities(data)
when (val federationVersion = data.federationVersion) {
is Loading,
is Uninitialized ->
@ -63,7 +64,6 @@ class HomeserverSettingsController @Inject constructor(
is Success ->
buildFederationVersion(federationVersion())
}
buildCapabilities(data)
}
private fun buildHeader(state: HomeServerSettingsViewState) {