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.
*/
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.mCryptoError = mCryptoError
it.mCryptoErrorReason = mCryptoErrorReason
@ -429,7 +458,7 @@ fun Event.isReplyRenderedInThread(): Boolean {
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

View file

@ -597,26 +597,22 @@ internal class LocalEchoEventFactory @Inject constructor(
return clock.epochMillis()
}
/**
* Creates a reply to a regular timeline Event or a thread Event if needed.
*/
fun createReplyTextEvent(
roomId: String,
fun createReplyTextContent(
eventReplied: TimelineEvent,
replyText: CharSequence,
replyTextFormatted: CharSequence?,
autoMarkdown: Boolean,
rootThreadEventId: String? = null,
showInThread: Boolean,
additionalContent: Content? = null
): Event? {
isRedactedEvent: Boolean = false
): MessageContent? {
// Fallbacks and event representation
// TODO Add error/warning logs when any of this is null
val permalink = permalinkFactory.createPermalink(eventReplied.root, false) ?: return null
val userId = eventReplied.root.senderId ?: 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.
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 eventId = eventReplied.root.eventId ?: return null
val content = MessageTextContent(
return MessageTextContent(
msgType = MessageType.MSGTYPE_TEXT,
format = MessageFormat.FORMAT_MATRIX_HTML,
body = replyFallback,
@ -646,7 +642,25 @@ internal class LocalEchoEventFactory @Inject constructor(
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) =
@ -715,7 +729,7 @@ internal class LocalEchoEventFactory @Inject constructor(
* 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.
*/
private fun bodyForReply(content: MessageContent?, isReply: Boolean): TextContent {
fun bodyForReply(content: MessageContent?, isReply: Boolean, isRedactedEvent: Boolean = false): TextContent {
when (content?.msgType) {
MessageType.MSGTYPE_EMOTE,
MessageType.MSGTYPE_TEXT,
@ -724,7 +738,9 @@ internal class LocalEchoEventFactory @Inject constructor(
if (content is MessageContentWithFormattedBody) {
formattedText = content.matrixFormattedBody
}
return if (isReply) {
return if (isRedactedEvent) {
TextContent("message removed.")
} else if (isReply) {
TextContent(content.body, formattedText).removeInReplyFallbacks()
} else {
TextContent(content.body, formattedText)
@ -738,7 +754,11 @@ internal class LocalEchoEventFactory @Inject constructor(
MessageType.MSGTYPE_POLL_START -> {
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.session.room.membership.LoadRoomMembersTask
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.sync.handler.room.ReadReceiptHandler
import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
@ -63,6 +64,7 @@ internal class DefaultTimeline(
private val settings: TimelineSettings,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val clock: Clock,
localEchoEventFactory: LocalEchoEventFactory,
stateEventDataSource: StateEventDataSource,
paginationTask: PaginationTask,
getEventTask: GetContextOfEventTask,
@ -114,6 +116,7 @@ internal class DefaultTimeline(
onNewTimelineEvents = this::onNewTimelineEvents,
stateEventDataSource = stateEventDataSource,
matrixCoroutineDispatchers = coroutineDispatchers,
localEchoEventFactory = localEchoEventFactory
)
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.session.room.membership.LoadRoomMembersTask
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.sync.handler.room.ReadReceiptHandler
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 clock: Clock,
private val stateEventDataSource: StateEventDataSource,
private val localEchoEventFactory: LocalEchoEventFactory
) : TimelineService {
@AssistedFactory
@ -82,6 +84,7 @@ internal class DefaultTimelineService @AssistedInject constructor(
lightweightSettingsStorage = lightweightSettingsStorage,
clock = clock,
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.where
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.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.util.time.Clock
import timber.log.Timber
@ -106,6 +111,7 @@ internal class LoadTimelineStrategy constructor(
val onNewTimelineEvents: (List<String>) -> Unit,
val stateEventDataSource: StateEventDataSource,
val matrixCoroutineDispatchers: MatrixCoroutineDispatchers,
val localEchoEventFactory: LocalEchoEventFactory
)
private var getContextLatch: CompletableDeferred<Unit>? = null
@ -323,6 +329,19 @@ internal class LoadTimelineStrategy constructor(
}
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 TimelineChunk(
chunkEntity = it,
@ -341,6 +360,9 @@ internal class LoadTimelineStrategy constructor(
initialEventId = mode.originEventId(),
onBuiltEvents = dependencies.onEventsUpdated,
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.OrderedRealmCollectionChangeListener
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.RealmObjectChangeListener
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.tryOrNull
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.TimelineEvent
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.internal.database.mapper.EventMapper
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.session.room.relation.threads.DefaultFetchThreadTimelineTask
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 timber.log.Timber
import java.util.Collections
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
/**
* This is a wrapper around a ChunkEntity in the database.
@ -66,6 +72,9 @@ internal class TimelineChunk(
private val initialEventId: String?,
private val onBuiltEvents: (Boolean) -> Unit,
private val onEventsDeleted: () -> Unit,
private val realm: AtomicReference<Realm>,
private val decorator: TimelineEventDecorator,
val localEchoEventFactory: LocalEchoEventFactory,
) {
private val isLastForward = AtomicBoolean(chunkEntity.isLastForward)
@ -74,6 +83,13 @@ internal class TimelineChunk(
private var prevChunkLatch: 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 ->
if (changeSet == null) return@RealmObjectChangeListener
if (changeSet.isDeleted.orFalse()) {
@ -353,9 +369,6 @@ internal class TimelineChunk(
timelineEvents
.mapIndexed { index, timelineEventEntity ->
val timelineEvent = timelineEventEntity.buildAndDecryptIfNeeded()
if (timelineEvent.root.type == EventType.STATE_ROOM_CREATE) {
isLastBackward.set(true)
}
if (direction == Timeline.Direction.FORWARDS) {
builtEventsIndexes[timelineEvent.eventId] = index
builtEvents.add(index, timelineEvent)
@ -394,26 +407,45 @@ internal class TimelineChunk(
}
private fun TimelineEventEntity.buildAndDecryptIfNeeded(): TimelineEvent {
val timelineEvent = buildTimelineEvent(this)
val transactionId = timelineEvent.root.unsignedData?.transactionId
uiEchoManager?.onSyncedEvent(transactionId)
if (timelineEvent.isEncrypted() &&
timelineEvent.root.mxDecryptionResult == null) {
timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineId)) }
/**
* Makes sure to update some internal state after a TimelineEvent is built.
*/
fun processTimelineEvent(timelineEvent: TimelineEvent) {
if (timelineEvent.root.type == EventType.STATE_ROOM_CREATE) {
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
timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineId)) }
fun decryptIfNeeded(timelineEvent: TimelineEvent) {
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(
timelineEventEntity = eventEntity,
buildReadReceipts = timelineSettings.buildReadReceipts
).let {
// eventually enhance with ui echo?
(uiEchoManager?.decorateEventWithReactionUiEcho(it) ?: it)
).let { timelineEvent ->
decorator.decorate(timelineEvent)
}
/**
@ -493,13 +525,9 @@ internal class TimelineChunk(
if (!validateInsertion(range, results)) continue
val newItems = results
.subList(range.startIndex, range.startIndex + range.length)
.map { it.buildAndDecryptIfNeeded() }
builtEventsIndexes.entries.filter { it.value >= range.startIndex }.forEach { it.setValue(it.value + range.length) }
newItems.mapIndexed { index, timelineEvent ->
if (timelineEvent.root.type == EventType.STATE_ROOM_CREATE) {
isLastBackward.set(true)
}
newItems.mapIndexed { index, timelineEventEntity ->
val timelineEvent = timelineEventEntity.buildAndDecryptIfNeeded()
val correctedIndex = range.startIndex + index
builtEvents.add(correctedIndex, timelineEvent)
builtEventsIndexes[timelineEvent.eventId] = correctedIndex
@ -509,11 +537,17 @@ internal class TimelineChunk(
for (range in modifications) {
for (modificationIndex in (range.startIndex until range.startIndex + range.length)) {
val updatedEntity = results[modificationIndex] ?: continue
val builtEventIndex = builtEventsIndexes[updatedEntity.eventId] ?: continue
try {
builtEvents[builtEventIndex] = updatedEntity.buildAndDecryptIfNeeded()
} catch (failure: Throwable) {
Timber.v("Fail to update items at index: $modificationIndex")
val updatedEventId = updatedEntity.eventId
val repliesOfUpdatedEvent = repliedEventsMap.getOrElse(updatedEventId) { emptySet() }.mapNotNull { eventId ->
results.where().equalTo(TimelineEventEntityFields.EVENT_ID, eventId).findFirst()
}
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,
initialEventId = null,
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
)
}
}