mirror of
https://github.com/element-hq/element-android
synced 2024-11-27 20:06:51 +03: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
10 changed files with 323 additions and 44 deletions
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.
|
* 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
|
||||||
|
|
||||||
|
|
|
@ -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 ?: "")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 a new issue