mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-16 02:05:06 +08:00
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:
commit
5e971346ef
1
changelog.d/5546.bugfix
Normal file
1
changelog.d/5546.bugfix
Normal file
@ -0,0 +1 @@
|
||||
ReplyTo are not updated if the original message is edited or deleted.
|
@ -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
|
||||
|
||||
|
@ -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 ?: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user