- Refactor thread awareness (handle decrypted rooms, images, stickers etc)

- Enable/disable threads functionality
- New fallback thread implementation
This commit is contained in:
ariskotsomitopoulos 2022-01-24 16:55:15 +02:00
parent e0630ceac0
commit fe88e81d4a
24 changed files with 325 additions and 167 deletions

View file

@ -201,18 +201,19 @@ data class Event(
fun getDecryptedTextSummary(): String? {
val text = getDecryptedValue() ?: return null
return when {
isReply() || isQuote() -> ContentUtils.extractUsefulTextFromReply(text)
isReplyRenderedInThread() || isQuote() -> ContentUtils.extractUsefulTextFromReply(text)
isFileMessage() -> "sent a file."
isAudioMessage() -> "sent an audio file."
isImageMessage() -> "sent an image."
isVideoMessage() -> "sent a video."
isSticker() -> "sent a sticker"
isPoll() -> getPollQuestion() ?: "created a poll."
else -> text
}
}
private fun Event.isQuote(): Boolean {
if (isReply()) return false
if (isReplyRenderedInThread()) return false
return getDecryptedValue("formatted_body")?.contains("<blockquote>") ?: false
}
@ -374,6 +375,10 @@ fun Event.isReply(): Boolean {
return getRelationContent()?.inReplyTo?.eventId != null
}
fun Event.isReplyRenderedInThread(): Boolean {
return isReply() && getRelationContent()?.inReplyTo?.renderIn?.contains("m.thread") == true
}
fun Event.isThread(): Boolean = getRelationContentForType(RelationType.IO_THREAD)?.eventId != null
fun Event.getRootThreadEventId(): String? = getRelationContentForType(RelationType.IO_THREAD)?.eventId

View file

@ -20,7 +20,6 @@ import io.realm.Realm
import io.realm.RealmQuery
import io.realm.RealmResults
import io.realm.Sort
import org.matrix.android.sdk.BuildConfig
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.model.ChunkEntity
@ -40,7 +39,6 @@ internal fun Map<String, EventEntity>.updateThreadSummaryIfNeeded(
roomId: String,
realm: Realm, currentUserId: String,
shouldUpdateNotifications: Boolean = true) {
for ((rootThreadEventId, eventEntity) in this) {
eventEntity.findAllThreadsForRootEventId(eventEntity.realm, rootThreadEventId).let {
if (it.isNullOrEmpty()) return@let
@ -57,7 +55,7 @@ internal fun Map<String, EventEntity>.updateThreadSummaryIfNeeded(
}
}
if(shouldUpdateNotifications) {
if (shouldUpdateNotifications) {
updateNotificationsNew(roomId, realm, currentUserId)
}
}

View file

@ -125,9 +125,15 @@ internal fun EventEntity.asDomain(castJsonNumbers: Boolean = false): Event {
return EventMapper.map(this, castJsonNumbers)
}
internal fun Event.toEntity(roomId: String, sendState: SendState, ageLocalTs: Long?): EventEntity {
internal fun Event.toEntity(roomId: String, sendState: SendState, ageLocalTs: Long?, contentToInject: String? = null): EventEntity {
return EventMapper.map(this, roomId).apply {
this.sendState = sendState
this.ageLocalTs = ageLocalTs
contentToInject?.let {
this.content = it
if (this.type == EventType.STICKER) {
this.type = EventType.MESSAGE
}
}
}
}

View file

@ -20,7 +20,6 @@ import io.realm.RealmObject
import io.realm.annotations.Index
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
import org.matrix.android.sdk.internal.di.MoshiProvider
@ -80,10 +79,10 @@ internal open class EventEntity(@Index var eventId: String = "",
companion object
fun setDecryptionResult(result: MXEventDecryptionResult, clearEvent: JsonDict? = null) {
fun setDecryptionResult(result: MXEventDecryptionResult) {
assertIsManaged()
val decryptionResult = OlmDecryptionResult(
payload = clearEvent ?: result.clearEvent,
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain

View file

@ -83,11 +83,9 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor(
val threadList = response.chunks + listOfNotNull(response.originalEvent)
return storeNewEventsIfNeeded(threadList, params.roomId)
}
/**
* Store new events if they are not already received, and returns weather or not,
* a timeline update should be made
@ -105,7 +103,6 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor(
val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
for (event in threadList.reversed()) {
if (event.eventId == null || event.senderId == null || event.type == null) {
eventsSkipped++
continue
@ -180,7 +177,6 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor(
private fun handleReaction(realm: Realm,
event: Event,
roomId: String) {
val unsignedData = event.unsignedData ?: return
val relatedEventId = event.eventId ?: return
@ -206,7 +202,6 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor(
sum.count += 1
}
}
}
}
}

View file

@ -28,6 +28,7 @@ import org.matrix.android.sdk.api.session.events.model.LocalEcho
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.UnsignedData
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.message.AudioInfo
import org.matrix.android.sdk.api.session.room.model.message.AudioWaveformInfo
import org.matrix.android.sdk.api.session.room.model.message.FileInfo
@ -41,6 +42,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageFormat
import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
@ -293,7 +295,13 @@ internal class LocalEchoEventFactory @Inject constructor(
size = attachment.size
),
url = attachment.queryUri.toString(),
relatesTo = rootThreadEventId?.let { RelationDefaultContent(RelationType.IO_THREAD, it) }
relatesTo = rootThreadEventId?.let {
RelationDefaultContent(
type = RelationType.IO_THREAD,
eventId = it,
inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
)
}
)
return createMessageEvent(roomId, content)
}
@ -330,7 +338,13 @@ internal class LocalEchoEventFactory @Inject constructor(
thumbnailInfo = thumbnailInfo
),
url = attachment.queryUri.toString(),
relatesTo = rootThreadEventId?.let { RelationDefaultContent(RelationType.IO_THREAD, it) }
relatesTo = rootThreadEventId?.let {
RelationDefaultContent(
type = RelationType.IO_THREAD,
eventId = it,
inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
)
}
)
return createMessageEvent(roomId, content)
}
@ -354,7 +368,13 @@ internal class LocalEchoEventFactory @Inject constructor(
waveform = waveformSanitizer.sanitize(attachment.waveform)
),
voiceMessageIndicator = if (!isVoiceMessage) null else emptyMap(),
relatesTo = rootThreadEventId?.let { RelationDefaultContent(RelationType.IO_THREAD, it) }
relatesTo = rootThreadEventId?.let {
RelationDefaultContent(
type = RelationType.IO_THREAD,
eventId = it,
inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
)
}
)
return createMessageEvent(roomId, content)
}
@ -368,7 +388,13 @@ internal class LocalEchoEventFactory @Inject constructor(
size = attachment.size
),
url = attachment.queryUri.toString(),
relatesTo = rootThreadEventId?.let { RelationDefaultContent(RelationType.IO_THREAD, it) }
relatesTo = rootThreadEventId?.let {
RelationDefaultContent(
type = RelationType.IO_THREAD,
eventId = it,
inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
)
}
)
return createMessageEvent(roomId, content)
}
@ -378,6 +404,7 @@ internal class LocalEchoEventFactory @Inject constructor(
}
fun createEvent(roomId: String, type: String, content: Content?): Event {
val newContent = enhanceStickerIfNeeded(type, content) ?: content
val localId = LocalEcho.createLocalEchoId()
return Event(
roomId = roomId,
@ -385,11 +412,31 @@ internal class LocalEchoEventFactory @Inject constructor(
senderId = userId,
eventId = localId,
type = type,
content = content,
content = newContent,
unsignedData = UnsignedData(age = null, transactionId = localId)
)
}
/**
* Enhance sticker to support threads fallback if needed
*/
private fun enhanceStickerIfNeeded(type: String, content: Content?): Content? {
var newContent: Content? = null
if (type == EventType.STICKER) {
val isThread = (content.toModel<MessageStickerContent>())?.relatesTo?.type == RelationType.IO_THREAD
val rootThreadEventId = (content.toModel<MessageStickerContent>())?.relatesTo?.eventId
if (isThread && rootThreadEventId != null) {
val newRelationalDefaultContent = (content.toModel<MessageStickerContent>())?.relatesTo?.copy(
inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId))
)
newContent = (content.toModel<MessageStickerContent>())?.copy(
relatesTo = newRelationalDefaultContent
).toContent()
}
}
return newContent
}
/**
* Creates a thread event related to the already existing root event
*/
@ -404,7 +451,10 @@ internal class LocalEchoEventFactory @Inject constructor(
return createEvent(
roomId,
EventType.MESSAGE,
content.toThreadTextContent(rootThreadEventId, msgType)
content.toThreadTextContent(
rootThreadEventId = rootThreadEventId,
latestThreadEventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId),
msgType = msgType)
.toContent())
}
@ -471,8 +521,8 @@ internal class LocalEchoEventFactory @Inject constructor(
RelationDefaultContent(
type = RelationType.IO_THREAD,
eventId = it,
inReplyTo = ReplyToContent(eventId = eventId))
} ?: RelationDefaultContent(null, null, ReplyToContent( eventId = eventId))
inReplyTo = ReplyToContent(eventId = eventId, renderIn = arrayListOf("m.thread")))
} ?: RelationDefaultContent(null, null, ReplyToContent(eventId = eventId))
private fun buildFormattedReply(permalink: String, userLink: String, userId: String, bodyFormatted: String, newBodyFormatted: String): String {
return REPLY_PATTERN.format(
@ -584,7 +634,10 @@ internal class LocalEchoEventFactory @Inject constructor(
roomId,
markdownParser
.parse(quoteText, force = true, advanced = autoMarkdown)
.toThreadTextContent(rootThreadEventId, MessageType.MSGTYPE_TEXT)
.toThreadTextContent(
rootThreadEventId = rootThreadEventId,
latestThreadEventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId),
msgType = MessageType.MSGTYPE_TEXT)
)
} else {
createFormattedTextEvent(
@ -625,6 +678,7 @@ internal class LocalEchoEventFactory @Inject constructor(
// </mx-reply>
// No whitespace because currently breaks temporary formatted text to Span
const val REPLY_PATTERN = """<mx-reply><blockquote><a href="%s">In reply to</a> <a href="%s">%s</a><br />%s</blockquote></mx-reply>%s"""
const val QUOTE_PATTERN = """<blockquote><p>%s</p></blockquote><p>%s</p>"""
// This is used to replace inner mx-reply tags
val MX_REPLY_REGEX = "<mx-reply>.*</mx-reply>".toRegex()

View file

@ -138,7 +138,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
}
}
fun deleteFailedEchoAsync(roomId: String, eventId: String?) {
fun deleteFailedEchoAsync(roomId: String, eventId: String?) {
monarchy.runTransactionSync { realm ->
TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId ?: "").findFirst()?.deleteFromRealm()
EventEntity.where(realm, eventId = eventId ?: "").findFirst()?.deleteFromRealm()
@ -215,4 +215,13 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
}
}
}
/**
* Returns the latest known thread event message, or the rootThreadEventId if no other event found
*/
fun getLatestThreadEvent(rootThreadEventId: String): String {
return realmSessionProvider.withRealm { realm ->
EventEntity.where(realm, eventId = rootThreadEventId).findFirst()?.threadSummaryLatestMessage?.eventId
} ?: rootThreadEventId
}
}

View file

@ -44,12 +44,25 @@ fun TextContent.toMessageTextContent(msgType: String = MessageType.MSGTYPE_TEXT)
)
}
fun TextContent.toThreadTextContent(rootThreadEventId: String, msgType: String = MessageType.MSGTYPE_TEXT): MessageTextContent {
/**
* Transform a TextContent to a thread message content. It will also add the inReplyTo
* latestThreadEventId in order for the clients without threads enabled to render it appropriately
* If latest event not found, we pass rootThreadEventId
*/
fun TextContent.toThreadTextContent(
rootThreadEventId: String,
latestThreadEventId: String,
msgType: String = MessageType.MSGTYPE_TEXT): MessageTextContent {
return MessageTextContent(
msgType = msgType,
format = MessageFormat.FORMAT_MATRIX_HTML.takeIf { formattedText != null },
body = text,
relatesTo = RelationDefaultContent(type = RelationType.IO_THREAD, eventId = rootThreadEventId, inReplyTo = ReplyToContent(eventId = "CYIpEhDXkImqKD2TF9NSocxt4vU6hh98yXi5Ncusdaw")),
relatesTo = RelationDefaultContent(
type = RelationType.IO_THREAD,
eventId = rootThreadEventId,
inReplyTo = ReplyToContent(
eventId = latestThreadEventId
)),
formattedBody = formattedText
)
}

View file

@ -36,6 +36,7 @@ import org.matrix.android.sdk.internal.database.model.ChunkEntity
import org.matrix.android.sdk.internal.database.model.ChunkEntityFields
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.query.where
import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
import timber.log.Timber
import java.util.Collections
@ -297,9 +298,9 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
if (timelineEvents.isEmpty()) return LoadedFromStorage()
// Disabled due to the new fallback
if(!lightweightSettingsStorage.areThreadMessagesEnabled()) {
fetchRootThreadEventsIfNeeded(timelineEvents)
}
// if(!lightweightSettingsStorage.areThreadMessagesEnabled()) {
// fetchRootThreadEventsIfNeeded(timelineEvents)
// }
if (direction == Timeline.Direction.FORWARDS) {
builtEventsIndexes.entries.forEach { it.setValue(it.value + timelineEvents.size) }
}
@ -353,6 +354,10 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
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 timelineEvent
}

View file

@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.internal.crypto.NewSessionListener
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase
@ -103,9 +104,27 @@ internal class TimelineEventDecryptor @Inject constructor(
}
}
private fun threadAwareNonEncryptedEvents(request: DecryptionRequest, realm: Realm) {
val event = request.event
realm.executeTransaction {
val eventId = event.eventId ?: return@executeTransaction
val eventEntity = EventEntity
.where(it, eventId = eventId)
.findFirst()
val decryptedEvent = eventEntity?.asDomain()
threadsAwarenessHandler.makeEventThreadAware(realm, event.roomId, decryptedEvent, eventEntity)
}
}
private fun processDecryptRequest(request: DecryptionRequest, realm: Realm) {
val event = request.event
val timelineId = request.timelineId
if (!request.event.isEncrypted()) {
// Here we have requested a decryption to an event that is not encrypted
// We will simply make this event thread aware
threadAwareNonEncryptedEvents(request, realm)
return
}
try {
val result = cryptoService.decryptEvent(request.event, timelineId)
Timber.v("Successfully decrypted event ${event.eventId}")
@ -114,21 +133,9 @@ internal class TimelineEventDecryptor @Inject constructor(
val eventEntity = EventEntity
.where(it, eventId = eventId)
.findFirst()
eventEntity?.apply {
val decryptedPayload =
// Disabled due to the new fallback
if (!lightweightSettingsStorage.areThreadMessagesEnabled()) {
threadsAwarenessHandler.handleIfNeededDuringDecryption(
it,
roomId = event.roomId,
event,
result)
} else {
null
}
setDecryptionResult(result, decryptedPayload)
}
eventEntity?.setDecryptionResult(result)
val decryptedEvent = eventEntity?.asDomain()
threadsAwarenessHandler.makeEventThreadAware(realm, event.roomId, decryptedEvent, eventEntity)
}
} catch (e: MXCryptoError) {
Timber.v("Failed to decrypt event ${event.eventId} : ${e.localizedMessage}")

View file

@ -184,7 +184,7 @@ internal class TokenChunkEventPersistor @Inject constructor(
}
liveEventManager.get().dispatchPaginatedEventReceived(event, roomId)
currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser)
if(lightweightSettingsStorage.areThreadMessagesEnabled()) {
if (lightweightSettingsStorage.areThreadMessagesEnabled()) {
eventEntity.rootThreadEventId?.let {
// This is a thread event
optimizedThreadSummaryMap[it] = eventEntity
@ -199,7 +199,7 @@ internal class TokenChunkEventPersistor @Inject constructor(
RoomEntity.where(realm, roomId).findFirst()?.addIfNecessary(currentChunk)
}
if(lightweightSettingsStorage.areThreadMessagesEnabled()) {
if (lightweightSettingsStorage.areThreadMessagesEnabled()) {
optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(roomId = roomId, realm = realm, currentUserId = userId)
}
}

View file

@ -104,9 +104,9 @@ internal class SyncResponseHandler @Inject constructor(
// Prerequisite for thread events handling in RoomSyncHandler
// Disabled due to the new fallback
if (!lightweightSettingsStorage.areThreadMessagesEnabled()) {
threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(syncResponse)
}
// if (!lightweightSettingsStorage.areThreadMessagesEnabled()) {
// threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(syncResponse)
// }
// Start one big transaction
monarchy.awaitTransaction { realm ->

View file

@ -381,16 +381,13 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
if (event.isEncrypted() && !isInitialSync) {
decryptIfNeeded(event, roomId)
}
// Disabled due to the new fallback
if (!lightweightSettingsStorage.areThreadMessagesEnabled()) {
threadsAwarenessHandler.handleIfNeeded(
realm = realm,
roomId = roomId,
event = event)
var contentToInject: String? = null
if (!isInitialSync) {
contentToInject = threadsAwarenessHandler.makeEventThreadAware(realm, roomId, event)
}
val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it }
val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType)
val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs, contentToInject).copyToRealmOrIgnore(realm, insertType)
if (event.stateKey != null) {
CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
eventId = event.eventId
@ -410,7 +407,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
}
chunkEntity.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser)
if(lightweightSettingsStorage.areThreadMessagesEnabled()) {
if (lightweightSettingsStorage.areThreadMessagesEnabled()) {
eventEntity.rootThreadEventId?.let {
// This is a thread event
optimizedThreadSummaryMap[it] = eventEntity
@ -447,7 +444,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
// Handle deletion of [stuck] local echos if needed
deleteLocalEchosIfNeeded(insertType, roomEntity, eventList)
if(lightweightSettingsStorage.areThreadMessagesEnabled()) {
if (lightweightSettingsStorage.areThreadMessagesEnabled()) {
optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(
roomId = roomId,
realm = realm,

View file

@ -18,11 +18,14 @@ package org.matrix.android.sdk.internal.session.sync.handler.room
import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import io.realm.kotlin.where
import org.matrix.android.sdk.api.session.events.model.Content
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.RelationType
import org.matrix.android.sdk.api.session.events.model.getRelationContentForType
import org.matrix.android.sdk.api.session.events.model.getRootThreadEventId
import org.matrix.android.sdk.api.session.events.model.isSticker
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.message.MessageFormat
@ -32,20 +35,23 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.sync.model.SyncResponse
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
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.asDomain
import org.matrix.android.sdk.internal.database.mapper.toEntity
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.EventEntityFields
import org.matrix.android.sdk.internal.database.model.EventInsertType
import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.MoshiProvider
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.permalinks.PermalinkFactory
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
import org.matrix.android.sdk.internal.session.room.timeline.GetEventTask
import org.matrix.android.sdk.internal.util.awaitTransaction
import timber.log.Timber
import javax.inject.Inject
/**
@ -55,11 +61,16 @@ import javax.inject.Inject
*/
internal class ThreadsAwarenessHandler @Inject constructor(
private val permalinkFactory: PermalinkFactory,
private val cryptoService: CryptoService,
@SessionDatabase private val monarchy: Monarchy,
private val lightweightSettingsStorage: LightweightSettingsStorage,
private val getEventTask: GetEventTask
) {
// This caching is responsible to improve the performance when we receive a root event
// to be able to know this event is a root one without checking the DB,
// We update the list with all thread root events by checking if there is a m.thread relation on the events
private val cacheEventRootId = hashSetOf<String>()
/**
* Fetch root thread events if they are missing from the local storage
* @param syncResponse the sync response
@ -142,120 +153,186 @@ internal class ThreadsAwarenessHandler @Inject constructor(
/**
* Handle events mainly coming from the RoomSyncHandler
* @return The content to inject in the roomSyncHandler live events
*/
fun handleIfNeeded(realm: Realm,
roomId: String,
event: Event) {
val payload = transformThreadToReplyIfNeeded(
realm = realm,
roomId = roomId,
event = event,
decryptedResult = event.mxDecryptionResult?.payload) ?: return
event.mxDecryptionResult = event.mxDecryptionResult?.copy(payload = payload)
}
/**
* Handle events while they are being decrypted
*/
fun handleIfNeededDuringDecryption(realm: Realm,
roomId: String?,
event: Event,
result: MXEventDecryptionResult): JsonDict? {
return transformThreadToReplyIfNeeded(
realm = realm,
roomId = roomId,
event = event,
decryptedResult = result.clearEvent)
}
/**
* If the event is a thread event then transform/enhance it to a visual Reply Event,
* If the event is not a thread event, null value will be returned
* If there is an error (ex. the root/origin thread event is not found), null will be returned
*/
private fun transformThreadToReplyIfNeeded(realm: Realm, roomId: String?, event: Event, decryptedResult: JsonDict?): JsonDict? {
fun makeEventThreadAware(realm: Realm,
roomId: String?,
event: Event?,
eventEntity: EventEntity? = null): String? {
event ?: return null
roomId ?: return null
if (lightweightSettingsStorage.areThreadMessagesEnabled()) return null
handleRootThreadEventsIfNeeded(realm, roomId, eventEntity, event)
if (!isThreadEvent(event)) return null
val rootThreadEventId = getRootThreadEventId(event) ?: return null
val payload = decryptedResult?.toMutableMap() ?: return null
var body = getValueFromPayload(payload, "body") ?: return null
val msgType = getValueFromPayload(payload, "msgtype") ?: run {
if (payload["type"]?.toString() == EventType.STICKER) {
MessageType.MSGTYPE_STICKER_LOCAL
} else {
return null
val eventPayload = if (!event.isEncrypted()) {
event.content?.toMutableMap() ?: return null
} else {
event.mxDecryptionResult?.payload?.toMutableMap() ?: return null
}
val eventBody = event.getDecryptedTextSummary() ?: return null
val eventIdToInject = getPreviousEventOrRoot(event) ?: run {
return@makeEventThreadAware injectFallbackIndicator(event, eventBody, eventEntity, eventPayload)
}
val eventToInject = getEventFromDB(realm, eventIdToInject)
val eventToInjectBody = eventToInject?.getDecryptedTextSummary()
var contentForNonEncrypted: String?
if (eventToInject != null && eventToInjectBody != null) {
// If the event to inject exists and is decrypted
// Inject it to our event
val messageTextContent = injectEvent(
roomId = roomId,
eventBody = eventBody,
eventToInject = eventToInject,
eventToInjectBody = eventToInjectBody) ?: return null
// update the event
contentForNonEncrypted = updateEventEntity(event, eventEntity, eventPayload, messageTextContent)
} else {
contentForNonEncrypted = injectFallbackIndicator(event, eventBody, eventEntity, eventPayload)
}
// Now lets try to find relations for improved results, while some events may come with reverse order
eventEntity?.let {
// When eventEntity is not null means that we are not from within roomSyncHandler
handleEventsThatRelatesTo(realm, roomId, event, eventBody, false)
}
return contentForNonEncrypted
}
/**
* Handle for not thread events that we have marked them as root.
* Find relations and inject them accordingly
* @param eventEntity the current eventEntity received
* @param event the current event received
* @return The content to inject in the roomSyncHandler live events
*/
private fun handleRootThreadEventsIfNeeded(realm: Realm, roomId: String, eventEntity: EventEntity?, event: Event): String? {
if (!isThreadEvent(event) && cacheEventRootId.contains(eventEntity?.eventId)) {
eventEntity?.let {
val eventBody = event.getDecryptedTextSummary() ?: return null
return handleEventsThatRelatesTo(realm, roomId, event, eventBody, true)
}
}
val rootThreadEvent = getEventFromDB(realm, rootThreadEventId) ?: return null
val rootThreadEventSenderId = rootThreadEvent.senderId ?: return null
return null
}
// Check the event type
when (msgType) {
MessageType.MSGTYPE_STICKER_LOCAL -> {
body = "sent a sticker from within a thread"
}
MessageType.MSGTYPE_FILE -> {
body = "sent a file from within a thread"
}
MessageType.MSGTYPE_VIDEO -> {
body = "Sent a video from within a thread"
}
MessageType.MSGTYPE_IMAGE -> {
body = "sent an image from within a thread"
}
MessageType.MSGTYPE_AUDIO -> {
body = "sent an audio file from within a thread"
}
/**
* This function is responsible to check if there is any event that relates to our current event
* This is useful when we receive an event that relates to a missing parent, so when later we receive the parent
* we can update the child as well
* @param event the current event that we examine
* @param eventBody the current body of the event
* @param isFromCache determines whether or not we already know this is root thread event
* @return The content to inject in the roomSyncHandler live events
*/
private fun handleEventsThatRelatesTo(realm: Realm, roomId: String, event: Event, eventBody: String, isFromCache: Boolean): String? {
event.eventId ?: return null
val rootThreadEventId = if (isFromCache) event.eventId else event.getRootThreadEventId() ?: return null
eventThatRelatesTo(realm, event.eventId, rootThreadEventId)?.forEach { eventEntityFound ->
val newEventFound = eventEntityFound.asDomain()
val newEventBody = newEventFound.getDecryptedTextSummary() ?: return null
val newEventPayload = newEventFound.mxDecryptionResult?.payload?.toMutableMap() ?: return null
val messageTextContent = injectEvent(
roomId = roomId,
eventBody = newEventBody,
eventToInject = event,
eventToInjectBody = eventBody) ?: return null
return updateEventEntity(newEventFound, eventEntityFound, newEventPayload, messageTextContent)
}
decryptIfNeeded(rootThreadEvent, roomId)
return null
}
val rootThreadEventBody = getValueFromPayload(rootThreadEvent.mxDecryptionResult?.payload?.toMutableMap(), "body")
/**
* Actual update the eventEntity with the new payload
* @return the content to inject when this is executed by RoomSyncHandler
*/
private fun updateEventEntity(event: Event,
eventEntity: EventEntity?,
eventPayload: MutableMap<String, Any>,
messageTextContent: Content): String? {
eventPayload["content"] = messageTextContent
val permalink = permalinkFactory.createPermalink(roomId, rootThreadEventId, false)
val userLink = permalinkFactory.createPermalink(rootThreadEventSenderId, false) ?: ""
if (event.isEncrypted()) {
if (event.isSticker()) {
eventPayload["type"] = EventType.MESSAGE
}
event.mxDecryptionResult = event.mxDecryptionResult?.copy(payload = eventPayload)
eventEntity?.decryptionResultJson = event.mxDecryptionResult?.let {
MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).toJson(it)
}
} else {
if (event.type == EventType.STICKER) {
eventEntity?.type = EventType.MESSAGE
}
eventEntity?.content = ContentMapper.map(messageTextContent)
return ContentMapper.map(messageTextContent)
}
return null
}
/**
* Injecting $eventToInject decrypted content as a reply to $event
* @param eventToInject the event that will inject
* @param eventBody the actual event body
* @return The final content with the injected event
*/
private fun injectEvent(roomId: String,
eventBody: String,
eventToInject: Event,
eventToInjectBody: String): Content? {
val eventToInjectId = eventToInject.eventId ?: return null
val eventIdToInjectSenderId = eventToInject.senderId.orEmpty()
val permalink = permalinkFactory.createPermalink(roomId, eventToInjectId, false)
val userLink = permalinkFactory.createPermalink(eventIdToInjectSenderId, false) ?: ""
val replyFormatted = LocalEchoEventFactory.REPLY_PATTERN.format(
permalink,
userLink,
rootThreadEventSenderId,
// Remove inner mx_reply tags if any
rootThreadEventBody,
body)
eventIdToInjectSenderId,
eventToInjectBody,
eventBody)
return MessageTextContent(
msgType = MessageType.MSGTYPE_TEXT,
format = MessageFormat.FORMAT_MATRIX_HTML,
body = eventBody,
formattedBody = replyFormatted
).toContent()
}
/**
* Integrate fallback Quote reply
*/
private fun injectFallbackIndicator(event: Event,
eventBody: String,
eventEntity: EventEntity?,
eventPayload: MutableMap<String, Any>): String? {
val replyFormatted = LocalEchoEventFactory.QUOTE_PATTERN.format(
"Replied within a thread",
eventBody)
val messageTextContent = MessageTextContent(
msgType = MessageType.MSGTYPE_TEXT,
format = MessageFormat.FORMAT_MATRIX_HTML,
body = body,
body = eventBody,
formattedBody = replyFormatted
).toContent()
payload["content"] = messageTextContent
return payload
return updateEventEntity(event, eventEntity, eventPayload, messageTextContent)
}
/**
* Decrypt the event
*/
private fun decryptIfNeeded(event: Event, roomId: String) {
try {
if (!event.isEncrypted() || event.mxDecryptionResult != null) return
// Event from sync does not have roomId, so add it to the event first
val result = cryptoService.decryptEvent(event.copy(roomId = roomId), "")
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
} catch (e: MXCryptoError) {
if (e is MXCryptoError.Base) {
event.mCryptoError = e.errorType
event.mCryptoErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription
}
private fun eventThatRelatesTo(realm: Realm, currentEventId: String, rootThreadEventId: String): List<EventEntity>? {
val threadList = realm.where<EventEntity>()
.beginGroup()
.equalTo(EventEntityFields.ROOT_THREAD_EVENT_ID, rootThreadEventId)
.or()
.equalTo(EventEntityFields.EVENT_ID, rootThreadEventId)
.endGroup()
.and()
.findAll()
cacheEventRootId.add(rootThreadEventId)
return threadList.filter {
it.asDomain().getRelationContentForType(RelationType.IO_THREAD)?.inReplyTo?.eventId == currentEventId
}
}
@ -281,9 +358,9 @@ internal class ThreadsAwarenessHandler @Inject constructor(
*/
private fun getRootThreadEventId(event: Event): String? =
event.content.toModel<MessageRelationContent>()?.relatesTo?.eventId
// private fun getRootThreadEventId(event: Event): String? =
// event.content.toModel<MessageRelationContent>()?.relatesTo?.inReplyTo?.eventId ?:
// event.content.toModel<MessageRelationContent>()?.relatesTo?.eventId
private fun getPreviousEventOrRoot(event: Event): String? =
event.content.toModel<MessageRelationContent>()?.relatesTo?.inReplyTo?.eventId
@Suppress("UNCHECKED_CAST")
private fun getValueFromPayload(payload: JsonDict?, key: String): String? {

View file

@ -83,7 +83,6 @@ import im.vector.app.features.themes.ThemeUtils
import im.vector.app.receivers.DebugReceiver
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.failure.GlobalError
import reactivecircus.flowbinding.android.view.clicks

View file

@ -21,7 +21,6 @@ import androidx.recyclerview.widget.RecyclerView
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.BuildConfig
import im.vector.app.features.autocomplete.AutocompleteClickListener
import im.vector.app.features.autocomplete.RecyclerViewPresenter
import im.vector.app.features.command.Command

View file

@ -16,7 +16,6 @@
package im.vector.app.features.command
import im.vector.app.BuildConfig
import im.vector.app.core.extensions.isEmail
import im.vector.app.core.extensions.isMsisdn
import im.vector.app.features.home.room.detail.ChatEffect

View file

@ -68,7 +68,6 @@ import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.vanniktech.emoji.EmojiPopup
import im.vector.app.BuildConfig
import im.vector.app.R
import im.vector.app.core.dialogs.ConfirmationDialogBuilder
import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper

View file

@ -282,6 +282,7 @@ class TimelineViewModel @AssistedInject constructor(
copy(myRoomMember = it)
}
}
private fun setupPreviewUrlObservers() {
if (!vectorPreferences.showUrlPreviews()) {
return
@ -488,6 +489,7 @@ class TimelineViewModel @AssistedInject constructor(
val content = initialState.rootThreadEventId?.let {
action.stickerContent.copy(relatesTo = RelationDefaultContent(RelationType.IO_THREAD, it))
} ?: action.stickerContent
room.sendEvent(EventType.STICKER, content.toContent())
}

View file

@ -22,7 +22,6 @@ import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.BuildConfig
import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.VectorEpoxyHolder

View file

@ -20,7 +20,6 @@ import dagger.Lazy
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.BuildConfig
import im.vector.app.R
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory

View file

@ -18,7 +18,6 @@ package im.vector.app.features.home.room.detail.timeline.helper
import im.vector.app.core.extensions.localDateTime
import im.vector.app.core.resources.UserPreferencesProvider
import org.matrix.android.sdk.BuildConfig
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.getRelationContent

View file

@ -29,7 +29,6 @@ import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import com.airbnb.epoxy.EpoxyAttribute
import im.vector.app.BuildConfig
import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.onClick

View file

@ -48,6 +48,5 @@ class VectorSettingsLabsFragment @Inject constructor(
false
}
}
}
}