Merge pull request #6704 from vector-im/cgizard/ISSUE-5546

Fix: ISSUE-5546: replyTo are not updated if the original message is edited
This commit is contained in:
Benoit Marty 2023-01-04 09:20:06 +01:00 committed by GitHub
commit 5e971346ef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 323 additions and 44 deletions

1
changelog.d/5546.bugfix Normal file
View file

@ -0,0 +1 @@
ReplyTo are not updated if the original message is edited or deleted.

View file

@ -126,8 +126,37 @@ data class Event(
/** /**
* Copy all fields, including transient fields. * Copy all fields, including transient fields.
*/ */
fun copyAll(): Event {
return copy().also { fun copyAll(
type: String? = this.type,
eventId: String? = this.eventId,
content: Content? = this.content,
prevContent: Content? = this.prevContent,
originServerTs: Long? = this.originServerTs,
senderId: String? = this.senderId,
stateKey: String? = this.stateKey,
roomId: String? = this.roomId,
unsignedData: UnsignedData? = this.unsignedData,
redacts: String? = this.redacts,
mxDecryptionResult: OlmDecryptionResult? = this.mxDecryptionResult,
mCryptoError: MXCryptoError.ErrorType? = this.mCryptoError,
mCryptoErrorReason: String? = this.mCryptoErrorReason,
sendState: SendState = this.sendState,
ageLocalTs: Long? = this.ageLocalTs,
threadDetails: ThreadDetails? = this.threadDetails,
): Event {
return copy(
type = type,
eventId = eventId,
content = content,
prevContent = prevContent,
originServerTs = originServerTs,
senderId = senderId,
stateKey = stateKey,
roomId = roomId,
unsignedData = unsignedData,
redacts = redacts
).also {
it.mxDecryptionResult = mxDecryptionResult it.mxDecryptionResult = mxDecryptionResult
it.mCryptoError = mCryptoError it.mCryptoError = mCryptoError
it.mCryptoErrorReason = mCryptoErrorReason it.mCryptoErrorReason = mCryptoErrorReason
@ -429,7 +458,7 @@ fun Event.isReplyRenderedInThread(): Boolean {
return isReply() && getRelationContent()?.shouldRenderInThread() == true return isReply() && getRelationContent()?.shouldRenderInThread() == true
} }
fun Event.isThread(): Boolean = getRelationContentForType(RelationType.THREAD)?.eventId != null fun Event.isThread(): Boolean = getRootThreadEventId() != null
fun Event.getRootThreadEventId(): String? = getRelationContentForType(RelationType.THREAD)?.eventId fun Event.getRootThreadEventId(): String? = getRelationContentForType(RelationType.THREAD)?.eventId

View file

@ -597,26 +597,22 @@ internal class LocalEchoEventFactory @Inject constructor(
return clock.epochMillis() return clock.epochMillis()
} }
/** fun createReplyTextContent(
* Creates a reply to a regular timeline Event or a thread Event if needed.
*/
fun createReplyTextEvent(
roomId: String,
eventReplied: TimelineEvent, eventReplied: TimelineEvent,
replyText: CharSequence, replyText: CharSequence,
replyTextFormatted: CharSequence?, replyTextFormatted: CharSequence?,
autoMarkdown: Boolean, autoMarkdown: Boolean,
rootThreadEventId: String? = null, rootThreadEventId: String? = null,
showInThread: Boolean, showInThread: Boolean,
additionalContent: Content? = null isRedactedEvent: Boolean = false
): Event? { ): MessageContent? {
// Fallbacks and event representation // Fallbacks and event representation
// TODO Add error/warning logs when any of this is null // TODO Add error/warning logs when any of this is null
val permalink = permalinkFactory.createPermalink(eventReplied.root, false) ?: return null val permalink = permalinkFactory.createPermalink(eventReplied.root, false) ?: return null
val userId = eventReplied.root.senderId ?: return null val userId = eventReplied.root.senderId ?: return null
val userLink = permalinkFactory.createPermalink(userId, false) ?: return null val userLink = permalinkFactory.createPermalink(userId, false) ?: return null
val body = bodyForReply(eventReplied.getLastMessageContent(), eventReplied.isReply()) val body = bodyForReply(eventReplied.getLastMessageContent(), eventReplied.isReply(), isRedactedEvent)
// As we always supply formatted body for replies we should force the MarkdownParser to produce html. // As we always supply formatted body for replies we should force the MarkdownParser to produce html.
val finalReplyTextFormatted = replyTextFormatted?.toString() ?: markdownParser.parse(replyText, force = true, advanced = autoMarkdown).takeFormatted() val finalReplyTextFormatted = replyTextFormatted?.toString() ?: markdownParser.parse(replyText, force = true, advanced = autoMarkdown).takeFormatted()
@ -635,7 +631,7 @@ internal class LocalEchoEventFactory @Inject constructor(
val replyFallback = buildReplyFallback(body, userId, replyText.toString()) val replyFallback = buildReplyFallback(body, userId, replyText.toString())
val eventId = eventReplied.root.eventId ?: return null val eventId = eventReplied.root.eventId ?: return null
val content = MessageTextContent( return MessageTextContent(
msgType = MessageType.MSGTYPE_TEXT, msgType = MessageType.MSGTYPE_TEXT,
format = MessageFormat.FORMAT_MATRIX_HTML, format = MessageFormat.FORMAT_MATRIX_HTML,
body = replyFallback, body = replyFallback,
@ -646,7 +642,25 @@ internal class LocalEchoEventFactory @Inject constructor(
showInThread = showInThread showInThread = showInThread
) )
) )
return createMessageEvent(roomId, content, additionalContent) }
/**
* Creates a reply to a regular timeline Event or a thread Event if needed.
*/
fun createReplyTextEvent(
roomId: String,
eventReplied: TimelineEvent,
replyText: CharSequence,
replyTextFormatted: CharSequence?,
autoMarkdown: Boolean,
rootThreadEventId: String? = null,
showInThread: Boolean,
additionalContent: Content? = null,
): Event? {
val content = createReplyTextContent(eventReplied, replyText, replyTextFormatted, autoMarkdown, rootThreadEventId, showInThread)
return content?.let {
createMessageEvent(roomId, it, additionalContent)
}
} }
private fun generateThreadRelationContent(rootThreadEventId: String) = private fun generateThreadRelationContent(rootThreadEventId: String) =
@ -715,7 +729,7 @@ internal class LocalEchoEventFactory @Inject constructor(
* In case of an edit of a reply the last content is not * In case of an edit of a reply the last content is not
* himself a reply, but it will contain the fallbacks, so we have to trim them. * himself a reply, but it will contain the fallbacks, so we have to trim them.
*/ */
private fun bodyForReply(content: MessageContent?, isReply: Boolean): TextContent { fun bodyForReply(content: MessageContent?, isReply: Boolean, isRedactedEvent: Boolean = false): TextContent {
when (content?.msgType) { when (content?.msgType) {
MessageType.MSGTYPE_EMOTE, MessageType.MSGTYPE_EMOTE,
MessageType.MSGTYPE_TEXT, MessageType.MSGTYPE_TEXT,
@ -724,7 +738,9 @@ internal class LocalEchoEventFactory @Inject constructor(
if (content is MessageContentWithFormattedBody) { if (content is MessageContentWithFormattedBody) {
formattedText = content.matrixFormattedBody formattedText = content.matrixFormattedBody
} }
return if (isReply) { return if (isRedactedEvent) {
TextContent("message removed.")
} else if (isReply) {
TextContent(content.body, formattedText).removeInReplyFallbacks() TextContent(content.body, formattedText).removeInReplyFallbacks()
} else { } else {
TextContent(content.body, formattedText) TextContent(content.body, formattedText)
@ -738,7 +754,11 @@ internal class LocalEchoEventFactory @Inject constructor(
MessageType.MSGTYPE_POLL_START -> { MessageType.MSGTYPE_POLL_START -> {
return TextContent((content as? MessagePollContent)?.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "") return TextContent((content as? MessagePollContent)?.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "")
} }
else -> return TextContent(content?.body ?: "") else -> {
return if (isRedactedEvent) {
TextContent("message removed.")
} else TextContent(content?.body ?: "")
}
} }
} }

View file

@ -42,6 +42,7 @@ import org.matrix.android.sdk.api.settings.LightweightSettingsStorage
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler
import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
@ -63,6 +64,7 @@ internal class DefaultTimeline(
private val settings: TimelineSettings, private val settings: TimelineSettings,
private val coroutineDispatchers: MatrixCoroutineDispatchers, private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val clock: Clock, private val clock: Clock,
localEchoEventFactory: LocalEchoEventFactory,
stateEventDataSource: StateEventDataSource, stateEventDataSource: StateEventDataSource,
paginationTask: PaginationTask, paginationTask: PaginationTask,
getEventTask: GetContextOfEventTask, getEventTask: GetContextOfEventTask,
@ -114,6 +116,7 @@ internal class DefaultTimeline(
onNewTimelineEvents = this::onNewTimelineEvents, onNewTimelineEvents = this::onNewTimelineEvents,
stateEventDataSource = stateEventDataSource, stateEventDataSource = stateEventDataSource,
matrixCoroutineDispatchers = coroutineDispatchers, matrixCoroutineDispatchers = coroutineDispatchers,
localEchoEventFactory = localEchoEventFactory
) )
private var strategy: LoadTimelineStrategy = buildStrategy(LoadTimelineStrategy.Mode.Live) private var strategy: LoadTimelineStrategy = buildStrategy(LoadTimelineStrategy.Mode.Live)

View file

@ -32,6 +32,7 @@ import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler
import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
@ -55,6 +56,7 @@ internal class DefaultTimelineService @AssistedInject constructor(
private val timelineEventDataSource: TimelineEventDataSource, private val timelineEventDataSource: TimelineEventDataSource,
private val clock: Clock, private val clock: Clock,
private val stateEventDataSource: StateEventDataSource, private val stateEventDataSource: StateEventDataSource,
private val localEchoEventFactory: LocalEchoEventFactory
) : TimelineService { ) : TimelineService {
@AssistedFactory @AssistedFactory
@ -82,6 +84,7 @@ internal class DefaultTimelineService @AssistedInject constructor(
lightweightSettingsStorage = lightweightSettingsStorage, lightweightSettingsStorage = lightweightSettingsStorage,
clock = clock, clock = clock,
stateEventDataSource = stateEventDataSource, stateEventDataSource = stateEventDataSource,
localEchoEventFactory = localEchoEventFactory
) )
} }

View file

@ -43,7 +43,12 @@ import org.matrix.android.sdk.internal.database.query.findAllIncludingEvents
import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfThread import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfThread
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
import org.matrix.android.sdk.internal.session.room.timeline.decorator.TimelineEventDecorator
import org.matrix.android.sdk.internal.session.room.timeline.decorator.TimelineEventDecoratorChain
import org.matrix.android.sdk.internal.session.room.timeline.decorator.UiEchoDecorator
import org.matrix.android.sdk.internal.session.room.timeline.decorator.UpdatedReplyDecorator
import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
import org.matrix.android.sdk.internal.util.time.Clock import org.matrix.android.sdk.internal.util.time.Clock
import timber.log.Timber import timber.log.Timber
@ -106,6 +111,7 @@ internal class LoadTimelineStrategy constructor(
val onNewTimelineEvents: (List<String>) -> Unit, val onNewTimelineEvents: (List<String>) -> Unit,
val stateEventDataSource: StateEventDataSource, val stateEventDataSource: StateEventDataSource,
val matrixCoroutineDispatchers: MatrixCoroutineDispatchers, val matrixCoroutineDispatchers: MatrixCoroutineDispatchers,
val localEchoEventFactory: LocalEchoEventFactory
) )
private var getContextLatch: CompletableDeferred<Unit>? = null private var getContextLatch: CompletableDeferred<Unit>? = null
@ -323,6 +329,19 @@ internal class LoadTimelineStrategy constructor(
} }
private fun RealmResults<ChunkEntity>.createTimelineChunk(): TimelineChunk? { private fun RealmResults<ChunkEntity>.createTimelineChunk(): TimelineChunk? {
fun createTimelineEventDecorator(): TimelineEventDecorator {
val decorators = listOf(
UiEchoDecorator(uiEchoManager),
UpdatedReplyDecorator(
realm = dependencies.realm,
roomId = roomId,
localEchoEventFactory = dependencies.localEchoEventFactory,
timelineEventMapper = dependencies.timelineEventMapper
)
)
return TimelineEventDecoratorChain(decorators)
}
return firstOrNull()?.let { return firstOrNull()?.let {
return TimelineChunk( return TimelineChunk(
chunkEntity = it, chunkEntity = it,
@ -341,6 +360,9 @@ internal class LoadTimelineStrategy constructor(
initialEventId = mode.originEventId(), initialEventId = mode.originEventId(),
onBuiltEvents = dependencies.onEventsUpdated, onBuiltEvents = dependencies.onEventsUpdated,
onEventsDeleted = dependencies.onEventsDeleted, onEventsDeleted = dependencies.onEventsDeleted,
realm = dependencies.realm,
localEchoEventFactory = dependencies.localEchoEventFactory,
decorator = createTimelineEventDecorator()
) )
} }
} }

View file

@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.timeline
import io.realm.OrderedCollectionChangeSet import io.realm.OrderedCollectionChangeSet
import io.realm.OrderedRealmCollectionChangeListener import io.realm.OrderedRealmCollectionChangeListener
import io.realm.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import io.realm.RealmObjectChangeListener import io.realm.RealmObjectChangeListener
import io.realm.RealmQuery import io.realm.RealmQuery
@ -27,9 +28,11 @@ import kotlinx.coroutines.CompletableDeferred
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.isReply
import org.matrix.android.sdk.api.session.room.timeline.Timeline 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.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import org.matrix.android.sdk.api.session.room.timeline.getRelationContent
import org.matrix.android.sdk.api.settings.LightweightSettingsStorage import org.matrix.android.sdk.api.settings.LightweightSettingsStorage
import org.matrix.android.sdk.internal.database.mapper.EventMapper import org.matrix.android.sdk.internal.database.mapper.EventMapper
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
@ -39,10 +42,13 @@ import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadTimelineTask import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadTimelineTask
import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
import org.matrix.android.sdk.internal.session.room.timeline.decorator.TimelineEventDecorator
import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
import timber.log.Timber import timber.log.Timber
import java.util.Collections import java.util.Collections
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
/** /**
* This is a wrapper around a ChunkEntity in the database. * This is a wrapper around a ChunkEntity in the database.
@ -66,6 +72,9 @@ internal class TimelineChunk(
private val initialEventId: String?, private val initialEventId: String?,
private val onBuiltEvents: (Boolean) -> Unit, private val onBuiltEvents: (Boolean) -> Unit,
private val onEventsDeleted: () -> Unit, private val onEventsDeleted: () -> Unit,
private val realm: AtomicReference<Realm>,
private val decorator: TimelineEventDecorator,
val localEchoEventFactory: LocalEchoEventFactory,
) { ) {
private val isLastForward = AtomicBoolean(chunkEntity.isLastForward) private val isLastForward = AtomicBoolean(chunkEntity.isLastForward)
@ -74,6 +83,13 @@ internal class TimelineChunk(
private var prevChunkLatch: CompletableDeferred<Unit>? = null private var prevChunkLatch: CompletableDeferred<Unit>? = null
private var nextChunkLatch: CompletableDeferred<Unit>? = null private var nextChunkLatch: CompletableDeferred<Unit>? = null
/**
Map of eventId -> eventId
The key holds the eventId of the repliedTo event.
The value holds a set of eventIds of all events replying to this event.
*/
private val repliedEventsMap = HashMap<String, MutableSet<String>>()
private val chunkObjectListener = RealmObjectChangeListener<ChunkEntity> { _, changeSet -> private val chunkObjectListener = RealmObjectChangeListener<ChunkEntity> { _, changeSet ->
if (changeSet == null) return@RealmObjectChangeListener if (changeSet == null) return@RealmObjectChangeListener
if (changeSet.isDeleted.orFalse()) { if (changeSet.isDeleted.orFalse()) {
@ -353,9 +369,6 @@ internal class TimelineChunk(
timelineEvents timelineEvents
.mapIndexed { index, timelineEventEntity -> .mapIndexed { index, timelineEventEntity ->
val timelineEvent = timelineEventEntity.buildAndDecryptIfNeeded() val timelineEvent = timelineEventEntity.buildAndDecryptIfNeeded()
if (timelineEvent.root.type == EventType.STATE_ROOM_CREATE) {
isLastBackward.set(true)
}
if (direction == Timeline.Direction.FORWARDS) { if (direction == Timeline.Direction.FORWARDS) {
builtEventsIndexes[timelineEvent.eventId] = index builtEventsIndexes[timelineEvent.eventId] = index
builtEvents.add(index, timelineEvent) builtEvents.add(index, timelineEvent)
@ -394,26 +407,45 @@ internal class TimelineChunk(
} }
private fun TimelineEventEntity.buildAndDecryptIfNeeded(): TimelineEvent { private fun TimelineEventEntity.buildAndDecryptIfNeeded(): TimelineEvent {
val timelineEvent = buildTimelineEvent(this) /**
val transactionId = timelineEvent.root.unsignedData?.transactionId * Makes sure to update some internal state after a TimelineEvent is built.
uiEchoManager?.onSyncedEvent(transactionId) */
if (timelineEvent.isEncrypted() && fun processTimelineEvent(timelineEvent: TimelineEvent) {
timelineEvent.root.mxDecryptionResult == null) { if (timelineEvent.root.type == EventType.STATE_ROOM_CREATE) {
timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineId)) } isLastBackward.set(true)
} else if (timelineEvent.root.isReply()) {
val relatesEventId = timelineEvent.getRelationContent()?.inReplyTo?.eventId
if (relatesEventId != null) {
val relatedEvents = repliedEventsMap.getOrPut(relatesEventId) { mutableSetOf() }
relatedEvents.add(timelineEvent.eventId)
}
}
val transactionId = timelineEvent.root.unsignedData?.transactionId
uiEchoManager?.onSyncedEvent(transactionId)
} }
if (!timelineEvent.isEncrypted() && !lightweightSettingsStorage.areThreadMessagesEnabled()) {
// Thread aware for not encrypted events fun decryptIfNeeded(timelineEvent: TimelineEvent) {
timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineId)) } if (timelineEvent.isEncrypted() &&
timelineEvent.root.mxDecryptionResult == null) {
timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineId)) }
}
if (!timelineEvent.isEncrypted() && !lightweightSettingsStorage.areThreadMessagesEnabled()) {
// Thread aware for not encrypted events
timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineId)) }
}
}
return buildTimelineEvent(this).also { timelineEvent ->
decryptIfNeeded(timelineEvent)
processTimelineEvent(timelineEvent)
} }
return timelineEvent
} }
private fun buildTimelineEvent(eventEntity: TimelineEventEntity) = timelineEventMapper.map( private fun buildTimelineEvent(eventEntity: TimelineEventEntity) = timelineEventMapper.map(
timelineEventEntity = eventEntity, timelineEventEntity = eventEntity,
buildReadReceipts = timelineSettings.buildReadReceipts buildReadReceipts = timelineSettings.buildReadReceipts
).let { ).let { timelineEvent ->
// eventually enhance with ui echo? decorator.decorate(timelineEvent)
(uiEchoManager?.decorateEventWithReactionUiEcho(it) ?: it)
} }
/** /**
@ -493,13 +525,9 @@ internal class TimelineChunk(
if (!validateInsertion(range, results)) continue if (!validateInsertion(range, results)) continue
val newItems = results val newItems = results
.subList(range.startIndex, range.startIndex + range.length) .subList(range.startIndex, range.startIndex + range.length)
.map { it.buildAndDecryptIfNeeded() }
builtEventsIndexes.entries.filter { it.value >= range.startIndex }.forEach { it.setValue(it.value + range.length) } builtEventsIndexes.entries.filter { it.value >= range.startIndex }.forEach { it.setValue(it.value + range.length) }
newItems.mapIndexed { index, timelineEvent -> newItems.mapIndexed { index, timelineEventEntity ->
if (timelineEvent.root.type == EventType.STATE_ROOM_CREATE) { val timelineEvent = timelineEventEntity.buildAndDecryptIfNeeded()
isLastBackward.set(true)
}
val correctedIndex = range.startIndex + index val correctedIndex = range.startIndex + index
builtEvents.add(correctedIndex, timelineEvent) builtEvents.add(correctedIndex, timelineEvent)
builtEventsIndexes[timelineEvent.eventId] = correctedIndex builtEventsIndexes[timelineEvent.eventId] = correctedIndex
@ -509,11 +537,17 @@ internal class TimelineChunk(
for (range in modifications) { for (range in modifications) {
for (modificationIndex in (range.startIndex until range.startIndex + range.length)) { for (modificationIndex in (range.startIndex until range.startIndex + range.length)) {
val updatedEntity = results[modificationIndex] ?: continue val updatedEntity = results[modificationIndex] ?: continue
val builtEventIndex = builtEventsIndexes[updatedEntity.eventId] ?: continue val updatedEventId = updatedEntity.eventId
try { val repliesOfUpdatedEvent = repliedEventsMap.getOrElse(updatedEventId) { emptySet() }.mapNotNull { eventId ->
builtEvents[builtEventIndex] = updatedEntity.buildAndDecryptIfNeeded() results.where().equalTo(TimelineEventEntityFields.EVENT_ID, eventId).findFirst()
} catch (failure: Throwable) { }
Timber.v("Fail to update items at index: $modificationIndex") repliesOfUpdatedEvent.plus(updatedEntity).forEach { entityToRebuild ->
val builtEventIndex = builtEventsIndexes[entityToRebuild.eventId] ?: return@forEach
try {
builtEvents[builtEventIndex] = entityToRebuild.buildAndDecryptIfNeeded()
} catch (failure: Throwable) {
Timber.v("Fail to update items at index: $modificationIndex")
}
} }
} }
} }
@ -580,7 +614,10 @@ internal class TimelineChunk(
lightweightSettingsStorage = lightweightSettingsStorage, lightweightSettingsStorage = lightweightSettingsStorage,
initialEventId = null, initialEventId = null,
onBuiltEvents = this.onBuiltEvents, onBuiltEvents = this.onBuiltEvents,
onEventsDeleted = this.onEventsDeleted onEventsDeleted = this.onEventsDeleted,
decorator = this.decorator,
realm = realm,
localEchoEventFactory = localEchoEventFactory
) )
} }

View file

@ -0,0 +1,42 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.room.timeline.decorator
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
/**
* This interface can be used to make a copy of a TimelineEvent with new data, before the event is posted to the timeline.
*/
internal fun interface TimelineEventDecorator {
fun decorate(timelineEvent: TimelineEvent): TimelineEvent
}
/**
* This is an implementation of [TimelineEventDecorator] which chains calls to decorators.
*/
internal class TimelineEventDecoratorChain(private val decorators: List<TimelineEventDecorator>) : TimelineEventDecorator {
override fun decorate(timelineEvent: TimelineEvent): TimelineEvent {
var decorated = timelineEvent
val iterator = decorators.iterator()
while (iterator.hasNext()) {
val decorator = iterator.next()
decorated = decorator.decorate(decorated)
}
return decorated
}
}

View file

@ -0,0 +1,27 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.room.timeline.decorator
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.internal.session.room.timeline.UIEchoManager
internal class UiEchoDecorator(private val uiEchoManager: UIEchoManager?) : TimelineEventDecorator {
override fun decorate(timelineEvent: TimelineEvent): TimelineEvent {
return uiEchoManager?.decorateEventWithReactionUiEcho(timelineEvent) ?: timelineEvent
}
}

View file

@ -0,0 +1,95 @@
/*
* Copyright (c) The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.room.timeline.decorator
import io.realm.Realm
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.isThread
import org.matrix.android.sdk.api.session.events.model.toContent
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.isReply
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
import java.util.concurrent.atomic.AtomicReference
internal class UpdatedReplyDecorator(
private val realm: AtomicReference<Realm>,
private val roomId: String,
private val localEchoEventFactory: LocalEchoEventFactory,
private val timelineEventMapper: TimelineEventMapper,
) : TimelineEventDecorator {
override fun decorate(timelineEvent: TimelineEvent): TimelineEvent {
return if (timelineEvent.isReply() && !timelineEvent.root.isThread()) {
val newRepliedEvent = createNewRepliedEvent(timelineEvent) ?: return timelineEvent
timelineEvent.copy(root = newRepliedEvent)
} else {
timelineEvent
}
}
private fun createNewRepliedEvent(currentTimelineEvent: TimelineEvent): Event? {
val relatesEventId = currentTimelineEvent.getRelationContent()?.inReplyTo?.eventId ?: return null
val timelineEventEntity = TimelineEventEntity.where(
realm.get(),
roomId,
relatesEventId
).findFirst() ?: return null
val isRedactedEvent = timelineEventEntity.root?.asDomain()?.isRedacted() ?: false
val replyText = localEchoEventFactory
.bodyForReply(currentTimelineEvent.getLastMessageContent(), true).formattedText ?: ""
val newContent = localEchoEventFactory.createReplyTextContent(
timelineEventMapper.map(timelineEventEntity),
replyText,
null,
false,
showInThread = false,
isRedactedEvent = isRedactedEvent
).toContent()
val decryptionResultToSet = currentTimelineEvent.root.mxDecryptionResult?.copy(
payload = mapOf(
"content" to newContent,
"type" to EventType.MESSAGE
)
)
val contentToSet = if (currentTimelineEvent.isEncrypted()) {
// Keep encrypted content as is
currentTimelineEvent.root.content
} else {
// Use new content
newContent
}
return currentTimelineEvent.root.copyAll(
content = contentToSet,
mxDecryptionResult = decryptionResultToSet,
mCryptoError = null,
mCryptoErrorReason = null
)
}
}