Merge pull request #5627 from vector-im/feature/aris/threads_summary_event_redaction

Thread redaction will now update the thread summary counter
This commit is contained in:
Aris Kotsomitopoulos 2022-03-31 16:21:35 +03:00 committed by GitHub
commit 3c06d56b87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 87 additions and 13 deletions

View File

@ -46,3 +46,5 @@ data class UnsignedData(
@Json(name = "replaces_state") val replacesState: String? = null @Json(name = "replaces_state") val replacesState: String? = null
) )
fun UnsignedData?.isRedacted() = this?.redactedEvent != null

View File

@ -16,9 +16,12 @@
package org.matrix.android.sdk.internal.database.helper package org.matrix.android.sdk.internal.database.helper
import com.squareup.moshi.JsonDataException
import io.realm.Realm import io.realm.Realm
import io.realm.RealmQuery import io.realm.RealmQuery
import io.realm.Sort 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.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
import org.matrix.android.sdk.internal.database.mapper.asDomain 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.findLastForwardChunkOfRoom
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.database.query.whereRoomId 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<Int, TimelineEventEntity>? private typealias Summary = Pair<Int, TimelineEventEntity>?
@ -48,14 +53,14 @@ internal fun Map<String, EventEntity>.updateThreadSummaryIfNeeded(
for ((rootThreadEventId, eventEntity) in this) { for ((rootThreadEventId, eventEntity) in this) {
eventEntity.threadSummaryInThread(eventEntity.realm, rootThreadEventId, chunkEntity)?.let { threadSummary -> eventEntity.threadSummaryInThread(eventEntity.realm, rootThreadEventId, chunkEntity)?.let { threadSummary ->
val numberOfMessages = threadSummary.first val inThreadMessages = threadSummary.first
val latestEventInThread = threadSummary.second val latestEventInThread = threadSummary.second
// If this is a thread message, find its root event if exists // If this is a thread message, find its root event if exists
val rootThreadEvent = if (eventEntity.isThread()) eventEntity.findRootThreadEvent() else eventEntity val rootThreadEvent = if (eventEntity.isThread()) eventEntity.findRootThreadEvent() else eventEntity
rootThreadEvent?.markEventAsRoot( rootThreadEvent?.markEventAsRoot(
threadsCounted = numberOfMessages, inThreadMessages = inThreadMessages,
latestMessageTimelineEventEntity = latestEventInThread latestMessageTimelineEventEntity = latestEventInThread
) )
} }
@ -81,28 +86,27 @@ internal fun EventEntity.findRootThreadEvent(): EventEntity? =
* Mark or update the current event a root thread event * Mark or update the current event a root thread event
*/ */
internal fun EventEntity.markEventAsRoot( internal fun EventEntity.markEventAsRoot(
threadsCounted: Int, inThreadMessages: Int,
latestMessageTimelineEventEntity: TimelineEventEntity?) { latestMessageTimelineEventEntity: TimelineEventEntity?) {
isRootThread = true isRootThread = true
numberOfThreads = threadsCounted numberOfThreads = inThreadMessages
threadSummaryLatestMessage = latestMessageTimelineEventEntity threadSummaryLatestMessage = latestMessageTimelineEventEntity
} }
/** /**
* Count the number of threads for the provided root thread eventId, and finds the latest event message * 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 * @param rootThreadEventId The root eventId that will find the number of threads
* @return A ThreadSummary containing the counted threads and the latest event message * @return A ThreadSummary containing the counted threads and the latest event message
*/ */
internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId: String, chunkEntity: ChunkEntity?): Summary { internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId: String, chunkEntity: ChunkEntity?): Summary {
// Number of messages val inThreadMessages = countInThreadMessages(
val messages = TimelineEventEntity realm = realm,
.whereRoomId(realm, roomId = roomId) roomId = roomId,
.equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId) rootThreadEventId = rootThreadEventId
.distinct(TimelineEventEntityFields.ROOT.EVENT_ID) )
.count()
.toInt()
if (messages <= 0) return null if (inThreadMessages <= 0) return null
// Find latest thread event, we know it exists // Find latest thread event, we know it exists
var chunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId) ?: chunkEntity ?: return null var chunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId) ?: chunkEntity ?: return null
@ -124,9 +128,38 @@ internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId:
result ?: return null 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 * 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 * exact chunk sequence while currentChunk is not yet committed in the DB

View File

@ -44,6 +44,7 @@ internal open class EventEntity(@Index var eventId: String = "",
// Thread related, no need to create a new Entity for performance // Thread related, no need to create a new Entity for performance
@Index var isRootThread: Boolean = false, @Index var isRootThread: Boolean = false,
@Index var rootThreadEventId: String? = null, @Index var rootThreadEventId: String? = null,
// Number messages within the thread
var numberOfThreads: Int = 0, var numberOfThreads: Int = 0,
var threadSummaryLatestMessage: TimelineEventEntity? = null var threadSummaryLatestMessage: TimelineEventEntity? = null
) : RealmObject() { ) : RealmObject() {

View File

@ -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.EventType
import org.matrix.android.sdk.api.session.events.model.LocalEcho 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.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.ContentMapper
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.model.EventEntity 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.EventInsertType
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity 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.findWithSenderMembershipEvent
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.MoshiProvider 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.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified)
eventToPrune.decryptionResultJson = null eventToPrune.decryptionResultJson = null
eventToPrune.decryptionErrorCode = null eventToPrune.decryptionErrorCode = null
handleTimelineThreadSummaryIfNeeded(realm, eventToPrune, isLocalEcho)
} }
// EventType.REACTION -> { // EventType.REACTION -> {
// eventRelationsAggregationUpdater.handleReactionRedact(eventToPrune, realm, userId) // 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<String> { private fun computeAllowedKeys(type: String): List<String> {
// Add filtered content, allowed keys in content depends on the event type // Add filtered content, allowed keys in content depends on the event type
return when (type) { return when (type) {