diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/UnsignedData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/UnsignedData.kt index dfe1db7b1c..630a2fb91a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/UnsignedData.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/UnsignedData.kt @@ -46,3 +46,5 @@ data class UnsignedData( @Json(name = "replaces_state") val replacesState: String? = null ) + +fun UnsignedData?.isRedacted() = this?.redactedEvent != null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt index ee3008d40b..04cf5b78af 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt @@ -16,9 +16,12 @@ package org.matrix.android.sdk.internal.database.helper +import com.squareup.moshi.JsonDataException import io.realm.Realm import io.realm.RealmQuery import io.realm.Sort +import org.matrix.android.sdk.api.session.events.model.UnsignedData +import org.matrix.android.sdk.api.session.events.model.isRedacted import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.threads.ThreadNotificationState import org.matrix.android.sdk.internal.database.mapper.asDomain @@ -33,6 +36,8 @@ import org.matrix.android.sdk.internal.database.query.findIncludingEvent import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.whereRoomId +import org.matrix.android.sdk.internal.di.MoshiProvider +import timber.log.Timber private typealias Summary = Pair? @@ -48,14 +53,14 @@ internal fun Map.updateThreadSummaryIfNeeded( for ((rootThreadEventId, eventEntity) in this) { eventEntity.threadSummaryInThread(eventEntity.realm, rootThreadEventId, chunkEntity)?.let { threadSummary -> - val numberOfMessages = threadSummary.first + val inThreadMessages = threadSummary.first val latestEventInThread = threadSummary.second // If this is a thread message, find its root event if exists val rootThreadEvent = if (eventEntity.isThread()) eventEntity.findRootThreadEvent() else eventEntity rootThreadEvent?.markEventAsRoot( - threadsCounted = numberOfMessages, + inThreadMessages = inThreadMessages, latestMessageTimelineEventEntity = latestEventInThread ) } @@ -81,28 +86,27 @@ internal fun EventEntity.findRootThreadEvent(): EventEntity? = * Mark or update the current event a root thread event */ internal fun EventEntity.markEventAsRoot( - threadsCounted: Int, + inThreadMessages: Int, latestMessageTimelineEventEntity: TimelineEventEntity?) { isRootThread = true - numberOfThreads = threadsCounted + numberOfThreads = inThreadMessages threadSummaryLatestMessage = latestMessageTimelineEventEntity } /** * Count the number of threads for the provided root thread eventId, and finds the latest event message + * note: Redactions are handled by RedactionEventProcessor * @param rootThreadEventId The root eventId that will find the number of threads * @return A ThreadSummary containing the counted threads and the latest event message */ internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId: String, chunkEntity: ChunkEntity?): Summary { - // Number of messages - val messages = TimelineEventEntity - .whereRoomId(realm, roomId = roomId) - .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId) - .distinct(TimelineEventEntityFields.ROOT.EVENT_ID) - .count() - .toInt() + val inThreadMessages = countInThreadMessages( + realm = realm, + roomId = roomId, + rootThreadEventId = rootThreadEventId + ) - if (messages <= 0) return null + if (inThreadMessages <= 0) return null // Find latest thread event, we know it exists var chunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId) ?: chunkEntity ?: return null @@ -124,9 +128,38 @@ internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId: result ?: return null - return Summary(messages, result) + return Summary(inThreadMessages, result) } +/** + * Counts the number of thread replies in the main timeline thread summary, + * with respect to redactions. + */ +internal fun countInThreadMessages(realm: Realm, roomId: String, rootThreadEventId: String): Int = + TimelineEventEntity + .whereRoomId(realm, roomId = roomId) + .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId) + .distinct(TimelineEventEntityFields.ROOT.EVENT_ID) + .findAll() + .filterNot { timelineEvent -> + timelineEvent.root + ?.unsignedData + ?.takeIf { it.isNotBlank() } + ?.toUnsignedData() + .isRedacted() + }.size + +/** + * Mapping string to UnsignedData using Moshi + */ +private fun String.toUnsignedData(): UnsignedData? = + try { + MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).fromJson(this) + } catch (ex: JsonDataException) { + Timber.e(ex, "Failed to parse UnsignedData") + null + } + /** * Lets compare them in case user is moving forward in the timeline and we cannot know the * exact chunk sequence while currentChunk is not yet committed in the DB diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt index b7158ba9cd..09be98aa96 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt @@ -44,6 +44,7 @@ internal open class EventEntity(@Index var eventId: String = "", // Thread related, no need to create a new Entity for performance @Index var isRootThread: Boolean = false, @Index var rootThreadEventId: String? = null, + // Number messages within the thread var numberOfThreads: Int = 0, var threadSummaryLatestMessage: TimelineEventEntity? = null ) : RealmObject() { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt index 4753e12157..b19b8d4a6b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt @@ -21,11 +21,14 @@ 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.LocalEcho import org.matrix.android.sdk.api.session.events.model.UnsignedData +import org.matrix.android.sdk.internal.database.helper.countInThreadMessages +import org.matrix.android.sdk.internal.database.helper.findRootThreadEvent import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.database.mapper.EventMapper import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventInsertType import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity import org.matrix.android.sdk.internal.database.query.findWithSenderMembershipEvent import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.MoshiProvider @@ -89,6 +92,8 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr eventToPrune.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified) eventToPrune.decryptionResultJson = null eventToPrune.decryptionErrorCode = null + + handleTimelineThreadSummaryIfNeeded(realm, eventToPrune, isLocalEcho) } // EventType.REACTION -> { // eventRelationsAggregationUpdater.handleReactionRedact(eventToPrune, realm, userId) @@ -104,6 +109,39 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr } } + /** + * Invalidates the number of threads in the main timeline thread summary, + * with respect to redactions. + */ + private fun handleTimelineThreadSummaryIfNeeded( + realm: Realm, + eventToPrune: EventEntity, + isLocalEcho: Boolean, + ) { + if (eventToPrune.isThread() && !isLocalEcho) { + val roomId = eventToPrune.roomId + val rootThreadEvent = eventToPrune.findRootThreadEvent() ?: return + val rootThreadEventId = eventToPrune.rootThreadEventId ?: return + + val inThreadMessages = countInThreadMessages( + realm = realm, + roomId = roomId, + rootThreadEventId = rootThreadEventId + ) + + rootThreadEvent.numberOfThreads = inThreadMessages + if (inThreadMessages == 0) { + // We should also clear the thread summary list + rootThreadEvent.isRootThread = false + rootThreadEvent.threadSummaryLatestMessage = null + ThreadSummaryEntity + .where(realm, roomId = roomId, rootThreadEventId) + .findFirst() + ?.deleteFromRealm() + } + } + } + private fun computeAllowedKeys(type: String): List { // Add filtered content, allowed keys in content depends on the event type return when (type) {