From 8f0074911aa6b6147d6ec08a8ecc174a6991d446 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Mon, 25 Oct 2021 19:00:39 +0300 Subject: [PATCH 001/190] Thread awareness, map threads events to replies --- .../network/ThreadToReplyMapInterceptor.kt | 105 +++++++++++++++++ .../sync/handler/room/RoomSyncHandler.kt | 3 + .../handler/room/ThreadsAwarenessHandler.kt | 108 ++++++++++++++++++ 3 files changed, 216 insertions(+) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ThreadToReplyMapInterceptor.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ThreadToReplyMapInterceptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ThreadToReplyMapInterceptor.kt new file mode 100644 index 0000000000..519e497e95 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ThreadToReplyMapInterceptor.kt @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2021 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.network + +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response +import okhttp3.ResponseBody +import okhttp3.ResponseBody.Companion.toResponseBody +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.network.ApiInterceptorListener +import org.matrix.android.sdk.api.network.ApiPath +import org.matrix.android.sdk.internal.di.MatrixScope +import timber.log.Timber +import javax.inject.Inject + +/** + * The goal of this interceptor is to map thread events to be handled as replies. + * The interceptor is responsible for mapping a thread event: + * "m.relates_to":{ + * "event_id":"$eventId", + * "rel_type":"io.element.thread" + * } + * to an equivalent reply event: + * m.relates_to":{ + * "m.in_reply_to":{ + * "event_id":"$eventId" + * } + */ +@MatrixScope +internal class ThreadToReplyMapInterceptor @Inject constructor() : Interceptor { + + init { + Timber.d("MapThreadToReplyInterceptor.init") + } + + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + + val response = chain.proceed(request) + + if (isSyncRequest(request)) { + Timber.i(" ------> found SYNC REQUEST") + + return response.body?.let { + val contentType = it.contentType() + val rawBody = it.string() + Timber.i(" ------> $rawBody") + + if(rawBody.contains("\"rel_type\":\"io.element.thread\"")){ + Timber.i(" ------> Thread found") + val start = rawBody.indexOf("\"rel_type\":\"io.element.thread\"") - "\"m.relates_to\":{\"event_id\":\"-GoMTnxkfmZczOPvbjcK43WqNib3wiJVaeO_vRxwHIDA\",\"".length +1 + val end = rawBody.indexOf("\"rel_type\":\"io.element.thread\"") + "\"rel_type\":\"io.element.thread\"".length +2 + val substr = rawBody.subSequence(start,end) + val newRaw = rawBody.replaceRange(start,end,"\"m.relates_to\":{\"m.in_reply_to\":{\"event_id\":\"\$HDddlX2bJQmVS0bN5R9HDzcrGDap18b3cFDDYjTjctc\"}},") + Timber.i(" ------> ${substr}") + Timber.i(" ------> new raw $newRaw") + val newBody = newRaw.toResponseBody(contentType) + response.newBuilder().body(newBody).build() + + }else{ + val newBody = rawBody.toResponseBody(contentType) + response.newBuilder().body(newBody).build() + } + } ?: response + +// response.peekBody(Long.MAX_VALUE).string().let { networkResponse -> +// Timber.i(" ------> ThreadToReplyMapInterceptor $networkResponse") +// } + } + +// val path = request.url.encodedPath +// if(path.contains("/sync/")){ +// Timber.i("-----> SYNC REQUEST --> $responseBody") +// +// +// } + +// val body = ResponseBody.create() +// val newResponse = response.newBuilder().body(body) + return response + } + + /** + * Returns true if the request is a sync request, false otherwise + * Example of a sync request: + * https://matrix-client.matrix.org/_matrix/client/r0/sync?filter=0&set_presence=online&t... + */ + private fun isSyncRequest(request: Request): Boolean = + ApiPath.SYNC.method == request.method && request.url.encodedPath.contains(ApiPath.SYNC.path) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt index 8c4af81c99..db4e07a21a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt @@ -76,6 +76,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle private val cryptoService: DefaultCryptoService, private val roomMemberEventHandler: RoomMemberEventHandler, private val roomTypingUsersHandler: RoomTypingUsersHandler, + private val threadsAwarenessHandler: ThreadsAwarenessHandler, private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, @UserId private val userId: String, private val timelineInput: TimelineInput) { @@ -366,6 +367,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle decryptIfNeeded(event, roomId) } + threadsAwarenessHandler.handleIfNeeded(realm, roomId, event, ::decryptIfNeeded) + val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType) if (event.stateKey != null) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt new file mode 100644 index 0000000000..cc01f62448 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt @@ -0,0 +1,108 @@ +/* + * Copyright 2021 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.sync.handler.room + +import io.realm.Realm +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.toContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageFormat +import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent +import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent +import org.matrix.android.sdk.api.session.room.model.tag.RoomTagContent +import org.matrix.android.sdk.api.util.JsonDict +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.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomTagEntity +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.session.permalinks.PermalinkFactory +import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory +import timber.log.Timber +import javax.inject.Inject + +/** + * This handler is responsible for a smooth threads migration. It will map all incoming + * threads as replies. So a device without threads enabled/updated will be able to view + * threads response as replies to the orighinal message + */ +internal class ThreadsAwarenessHandler @Inject constructor( + private val permalinkFactory: PermalinkFactory +) { + + fun handleIfNeeded(realm: Realm, + roomId: String, + event: Event, + decryptIfNeeded: (event: Event, roomId: String) -> Unit) { + + if (!isThreadEvent(event)) return + val rootThreadEventId = getRootThreadEventId(event) ?: return + val payload = event.mxDecryptionResult?.payload?.toMutableMap() ?: return + val body = getValueFromPayload(payload, "body") ?: return + val msgType = getValueFromPayload(payload, "msgtype") ?: return + val rootThreadEventEntity = EventEntity.where(realm, eventId = rootThreadEventId).findFirst() ?: return + val rootThreadEvent = EventMapper.map(rootThreadEventEntity) + val rootThreadEventSenderId = rootThreadEvent.senderId ?: return + val rootThreadEventEventId = rootThreadEvent.eventId ?: return + + Timber.i("------> Thread event detected!") + + if (rootThreadEvent.isEncrypted()) { + decryptIfNeeded(rootThreadEvent, roomId) + } + + val rootThreadEventBody = getValueFromPayload(rootThreadEvent.mxDecryptionResult?.payload?.toMutableMap(),"body") + + val permalink = permalinkFactory.createPermalink(roomId, rootThreadEventEventId, false) + val userLink = permalinkFactory.createPermalink(rootThreadEventSenderId, false) ?: "" + + val replyFormatted = LocalEchoEventFactory.REPLY_PATTERN.format( + permalink, + userLink, + rootThreadEventSenderId, + // Remove inner mx_reply tags if any + rootThreadEventBody, + body) + + val messageTextContent = MessageTextContent( + msgType = msgType, + format = MessageFormat.FORMAT_MATRIX_HTML, + body = body, + formattedBody = replyFormatted + ).toContent() + + payload["content"] = messageTextContent + + event.mxDecryptionResult = event.mxDecryptionResult?.copy(payload = payload ) + + } + + private fun isThreadEvent(event: Event): Boolean = + event.content.toModel()?.relatesTo?.type == "io.element.thread" + + private fun getRootThreadEventId(event: Event): String? = + event.content.toModel()?.relatesTo?.eventId + + @Suppress("UNCHECKED_CAST") + private fun getValueFromPayload(payload: JsonDict?, key: String): String? { + val content = payload?.get("content") as? JsonDict + return content?.get(key) as? String + } +} From d1f3e3f958d827d3d1a6ac0b1cd496757e36a3d4 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Tue, 26 Oct 2021 18:59:01 +0300 Subject: [PATCH 002/190] Thread awareness, map threads events to replies --- .../session/sync/handler/room/RoomSyncHandler.kt | 11 +++++++++-- .../sync/handler/room/ThreadsAwarenessHandler.kt | 8 ++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt index db4e07a21a..543783acac 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt @@ -363,11 +363,18 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle } eventIds.add(event.eventId) - if (event.isEncrypted() && insertType != EventInsertType.INITIAL_SYNC) { + val isInitialSync = insertType == EventInsertType.INITIAL_SYNC + + if (event.isEncrypted() && !isInitialSync) { decryptIfNeeded(event, roomId) } - threadsAwarenessHandler.handleIfNeeded(realm, roomId, event, ::decryptIfNeeded) + threadsAwarenessHandler.handleIfNeeded( + realm = realm, + roomId = roomId, + event = event, + isInitialSync = isInitialSync, + ::decryptIfNeeded) val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt index cc01f62448..1c45b16abf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt @@ -41,7 +41,7 @@ import javax.inject.Inject /** * This handler is responsible for a smooth threads migration. It will map all incoming * threads as replies. So a device without threads enabled/updated will be able to view - * threads response as replies to the orighinal message + * threads response as replies to the original message */ internal class ThreadsAwarenessHandler @Inject constructor( private val permalinkFactory: PermalinkFactory @@ -50,6 +50,7 @@ internal class ThreadsAwarenessHandler @Inject constructor( fun handleIfNeeded(realm: Realm, roomId: String, event: Event, + isInitialSync: Boolean, decryptIfNeeded: (event: Event, roomId: String) -> Unit) { if (!isThreadEvent(event)) return @@ -60,9 +61,8 @@ internal class ThreadsAwarenessHandler @Inject constructor( val rootThreadEventEntity = EventEntity.where(realm, eventId = rootThreadEventId).findFirst() ?: return val rootThreadEvent = EventMapper.map(rootThreadEventEntity) val rootThreadEventSenderId = rootThreadEvent.senderId ?: return - val rootThreadEventEventId = rootThreadEvent.eventId ?: return - Timber.i("------> Thread event detected!") + Timber.i("------> Thread event detected! - isInitialSync: $isInitialSync") if (rootThreadEvent.isEncrypted()) { decryptIfNeeded(rootThreadEvent, roomId) @@ -70,7 +70,7 @@ internal class ThreadsAwarenessHandler @Inject constructor( val rootThreadEventBody = getValueFromPayload(rootThreadEvent.mxDecryptionResult?.payload?.toMutableMap(),"body") - val permalink = permalinkFactory.createPermalink(roomId, rootThreadEventEventId, false) + val permalink = permalinkFactory.createPermalink(roomId, rootThreadEventId, false) val userLink = permalinkFactory.createPermalink(rootThreadEventSenderId, false) ?: "" val replyFormatted = LocalEchoEventFactory.REPLY_PATTERN.format( From 45a63b73bdc03e5051daef05566310e85ff218dd Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Tue, 2 Nov 2021 17:47:37 +0200 Subject: [PATCH 003/190] Make Android app thread aware. Handling also extreme cases like really old messages that the root thread message is not fetched in the device and initial sync --- .../internal/database/model/EventEntity.kt | 5 +- .../session/room/timeline/DefaultTimeline.kt | 22 ++ .../room/timeline/DefaultTimelineService.kt | 3 + .../room/timeline/TimelineEventDecryptor.kt | 19 +- .../session/sync/SyncResponseHandler.kt | 6 + .../sync/handler/room/RoomSyncHandler.kt | 4 +- .../handler/room/ThreadsAwarenessHandler.kt | 192 +++++++++++++++--- 7 files changed, 217 insertions(+), 34 deletions(-) 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 bcd30cb54b..836fc4efaf 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 @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.database.model 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.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 @@ -56,10 +57,10 @@ internal open class EventEntity(@Index var eventId: String = "", companion object - fun setDecryptionResult(result: MXEventDecryptionResult) { + fun setDecryptionResult(result: MXEventDecryptionResult, clearEvent: JsonDict? = null) { assertIsManaged() val decryptionResult = OlmDecryptionResult( - payload = result.clearEvent, + payload = clearEvent ?: result.clearEvent, senderKey = result.senderCurve25519Key, keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index 0c917448cc..e9f992f295 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -23,6 +23,7 @@ import io.realm.RealmConfiguration import io.realm.RealmQuery import io.realm.RealmResults import io.realm.Sort +import kotlinx.coroutines.runBlocking import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull @@ -33,6 +34,7 @@ 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.util.CancelableBag import org.matrix.android.sdk.internal.database.RealmSessionProvider +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.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.RoomEntity @@ -43,6 +45,7 @@ import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.whereRoomId import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask 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.task.TaskExecutor import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.util.Debouncer @@ -72,6 +75,7 @@ internal class DefaultTimeline( private val eventDecryptor: TimelineEventDecryptor, private val realmSessionProvider: RealmSessionProvider, private val loadRoomMembersTask: LoadRoomMembersTask, + private val threadsAwarenessHandler: ThreadsAwarenessHandler, private val readReceiptHandler: ReadReceiptHandler ) : Timeline, TimelineInput.Listener, @@ -577,6 +581,10 @@ internal class DefaultTimeline( } else { nextDisplayIndex = offsetIndex + 1 } + + // Prerequisite to in order for the ThreadsAwarenessHandler to work properly + fetchRootThreadEventsIfNeeded(offsetResults) + offsetResults.forEach { eventEntity -> val timelineEvent = buildTimelineEvent(eventEntity) @@ -601,6 +609,20 @@ internal class DefaultTimeline( return offsetResults.size } + /** + * This function is responsible to fetch and store the root event of a thread event + * in order to be able to display the event to the user appropriately + */ + private fun fetchRootThreadEventsIfNeeded(offsetResults: RealmResults) = runBlocking { + val eventEntityList = offsetResults + .mapNotNull { + it?.root + }.map { + EventMapper.map(it) + }.toList() + threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(eventEntityList) + } + private fun buildTimelineEvent(eventEntity: TimelineEventEntity): TimelineEvent { return timelineEventMapper.map( timelineEventEntity = eventEntity, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt index 47e8f7e3a3..75e7e774df 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt @@ -38,6 +38,7 @@ import org.matrix.android.sdk.internal.database.query.where 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.sync.handler.room.ReadReceiptHandler +import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler import org.matrix.android.sdk.internal.task.TaskExecutor internal class DefaultTimelineService @AssistedInject constructor( @@ -52,6 +53,7 @@ internal class DefaultTimelineService @AssistedInject constructor( private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, private val timelineEventMapper: TimelineEventMapper, private val loadRoomMembersTask: LoadRoomMembersTask, + private val threadsAwarenessHandler: ThreadsAwarenessHandler, private val readReceiptHandler: ReadReceiptHandler ) : TimelineService { @@ -75,6 +77,7 @@ internal class DefaultTimelineService @AssistedInject constructor( fetchTokenAndPaginateTask = fetchTokenAndPaginateTask, realmSessionProvider = realmSessionProvider, loadRoomMembersTask = loadRoomMembersTask, + threadsAwarenessHandler = threadsAwarenessHandler, readReceiptHandler = readReceiptHandler ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt index 721dae0b1b..75d02dfd98 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt @@ -26,6 +26,7 @@ import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent 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 +import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler import timber.log.Timber import java.util.concurrent.ExecutorService import java.util.concurrent.Executors @@ -34,7 +35,8 @@ import javax.inject.Inject internal class TimelineEventDecryptor @Inject constructor( @SessionDatabase private val realmConfiguration: RealmConfiguration, - private val cryptoService: CryptoService + private val cryptoService: CryptoService, + private val threadsAwarenessHandler: ThreadsAwarenessHandler ) { private val newSessionListener = object : NewSessionListener { @@ -106,10 +108,19 @@ internal class TimelineEventDecryptor @Inject constructor( val result = cryptoService.decryptEvent(request.event, timelineId) Timber.v("Successfully decrypted event ${event.eventId}") realm.executeTransaction { - val eventId = event.eventId ?: "" - EventEntity.where(it, eventId = eventId) + val eventId = event.eventId ?: return@executeTransaction + val eventEntity = EventEntity + .where(it, eventId = eventId) .findFirst() - ?.setDecryptionResult(result) + + eventEntity?.apply { + val decryptedPayload = threadsAwarenessHandler.handleIfNeededDuringDecryption( + it, + roomId = event.roomId, + event, + result) + setDecryptionResult(result, decryptedPayload) + } } } catch (e: MXCryptoError) { Timber.v("Failed to decrypt event ${event.eventId} : ${e.localizedMessage}") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt index 335f619623..98d6323bb1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt @@ -39,6 +39,7 @@ import org.matrix.android.sdk.internal.session.sync.handler.PresenceSyncHandler import org.matrix.android.sdk.internal.session.sync.handler.SyncResponsePostTreatmentAggregatorHandler import org.matrix.android.sdk.internal.session.sync.handler.UserAccountDataSyncHandler import org.matrix.android.sdk.internal.session.sync.handler.room.RoomSyncHandler +import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler import org.matrix.android.sdk.internal.util.awaitTransaction import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import timber.log.Timber @@ -62,6 +63,7 @@ internal class SyncResponseHandler @Inject constructor( private val tokenStore: SyncTokenStore, private val processEventForPushTask: ProcessEventForPushTask, private val pushRuleService: PushRuleService, + private val threadsAwarenessHandler: ThreadsAwarenessHandler, private val presenceSyncHandler: PresenceSyncHandler ) { @@ -94,6 +96,10 @@ internal class SyncResponseHandler @Inject constructor( Timber.v("Finish handling toDevice in $it ms") } val aggregator = SyncResponsePostTreatmentAggregator() + + // Prerequisite for thread events handling in RoomSyncHandler + threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(syncResponse) + // Start one big transaction monarchy.awaitTransaction { realm -> measureTimeMillis { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt index 543783acac..1a7e15e14c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt @@ -372,9 +372,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle threadsAwarenessHandler.handleIfNeeded( realm = realm, roomId = roomId, - event = event, - isInitialSync = isInitialSync, - ::decryptIfNeeded) + event = event) val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt index 1c45b16abf..12822aeb1d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt @@ -16,25 +16,32 @@ 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.events.model.Content +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.events.model.Event 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 import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent -import org.matrix.android.sdk.api.session.room.model.tag.RoomTagContent +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.mapper.EventMapper +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.EventInsertType -import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity -import org.matrix.android.sdk.internal.database.model.RoomTagEntity -import org.matrix.android.sdk.internal.database.query.getOrCreate +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.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.util.awaitTransaction import timber.log.Timber import javax.inject.Inject @@ -44,34 +51,137 @@ import javax.inject.Inject * threads response as replies to the original message */ internal class ThreadsAwarenessHandler @Inject constructor( - private val permalinkFactory: PermalinkFactory + private val permalinkFactory: PermalinkFactory, + private val cryptoService: CryptoService, + @SessionDatabase private val monarchy: Monarchy, + private val session: Session ) { + /** + * Fetch root thread events if they are missing from the local storage + * @param syncResponse the sync response + */ + suspend fun fetchRootThreadEventsIfNeeded(syncResponse: SyncResponse) { + val handlingStrategy = syncResponse.rooms?.join?.let { + RoomSyncHandler.HandlingStrategy.JOINED(it) + } + if (handlingStrategy !is RoomSyncHandler.HandlingStrategy.JOINED) return + val eventList = handlingStrategy.data + .mapNotNull { (roomId, roomSync) -> + roomSync.timeline?.events?.map { + it.copy(roomId = roomId) + } + }.flatten() + + fetchRootThreadEventsIfNeeded(eventList) + } + + /** + * Fetch root thread events if they are missing from the local storage + * @param eventList a list with the events to examine + */ + suspend fun fetchRootThreadEventsIfNeeded(eventList: List) { + if (eventList.isNullOrEmpty()) return + + val threadsToFetch = emptyMap().toMutableMap() + monarchy.awaitTransaction { realm -> + eventList.asSequence() + .filter { + isThreadEvent(it) && it.roomId != null + }.mapNotNull { event -> + getRootThreadEventId(event)?.let { + Pair(it, event.roomId!!) + } + }.forEach { (rootThreadEventId, roomId) -> + EventEntity.where(realm, rootThreadEventId).findFirst() ?: run { threadsToFetch[rootThreadEventId] = roomId } + } + } + fetchThreadsEvents(threadsToFetch) + } + + /** + * Fetch multiple unique events using the fetchEvent function + */ + private suspend fun fetchThreadsEvents(threadsToFetch: Map) { + val eventEntityList = threadsToFetch.mapNotNull { (eventId, roomId) -> + fetchEvent(eventId, roomId)?.let { + it.toEntity(roomId, SendState.SYNCED, it.ageLocalTs) + } + } + + if (eventEntityList.isNullOrEmpty()) return + + // Transaction should be done on its own thread, like below + monarchy.awaitTransaction { realm -> + eventEntityList.forEach { + it.copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC) + } + } + } + + /** + * This function will fetch the event from the homeserver, this is mandatory when the + * initial thread message is too old and is not saved in the device, so in order to + * construct the "reply to" format we need to know the event thread. + * @return the Event or null otherwise + */ + private suspend fun fetchEvent(eventId: String, roomId: String): Event? { + return runCatching { + Timber.i("------> Fetching event[$eventId]....") + session.getEvent(roomId = roomId, eventId = eventId) + }.fold( + onSuccess = { + it + }, + onFailure = { + null + }) + } + fun handleIfNeeded(realm: Realm, roomId: String, - event: Event, - isInitialSync: Boolean, - decryptIfNeeded: (event: Event, roomId: String) -> Unit) { + event: Event) { + val payload = transformThreadToReplyIfNeeded( + realm = realm, + roomId = roomId, + event = event, + decryptedResult = event.mxDecryptionResult?.payload) ?: return - if (!isThreadEvent(event)) return - val rootThreadEventId = getRootThreadEventId(event) ?: return - val payload = event.mxDecryptionResult?.payload?.toMutableMap() ?: return - val body = getValueFromPayload(payload, "body") ?: return - val msgType = getValueFromPayload(payload, "msgtype") ?: return - val rootThreadEventEntity = EventEntity.where(realm, eventId = rootThreadEventId).findFirst() ?: return - val rootThreadEvent = EventMapper.map(rootThreadEventEntity) - val rootThreadEventSenderId = rootThreadEvent.senderId ?: return + event.mxDecryptionResult = event.mxDecryptionResult?.copy(payload = payload) + } - Timber.i("------> Thread event detected! - isInitialSync: $isInitialSync") + fun handleIfNeededDuringDecryption(realm: Realm, + roomId: String?, + event: Event, + result: MXEventDecryptionResult): JsonDict? { + return transformThreadToReplyIfNeeded( + realm = realm, + roomId = roomId, + event = event, + decryptedResult = result.clearEvent) + } - if (rootThreadEvent.isEncrypted()) { - decryptIfNeeded(rootThreadEvent, roomId) - } + /** + * 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 willl be returend + */ + private fun transformThreadToReplyIfNeeded(realm: Realm, roomId: String?, event: Event, decryptedResult: JsonDict?): JsonDict? { + roomId ?: return null + if (!isThreadEvent(event)) return null + val rootThreadEventId = getRootThreadEventId(event) ?: return null + val payload = decryptedResult?.toMutableMap() ?: return null + val body = getValueFromPayload(payload, "body") ?: return null + val msgType = getValueFromPayload(payload, "msgtype") ?: return null + val rootThreadEvent = getEventFromDB(realm, rootThreadEventId) ?: return null + val rootThreadEventSenderId = rootThreadEvent.senderId ?: return null - val rootThreadEventBody = getValueFromPayload(rootThreadEvent.mxDecryptionResult?.payload?.toMutableMap(),"body") + decryptIfNeeded(rootThreadEvent, roomId) + + val rootThreadEventBody = getValueFromPayload(rootThreadEvent.mxDecryptionResult?.payload?.toMutableMap(), "body") val permalink = permalinkFactory.createPermalink(roomId, rootThreadEventId, false) - val userLink = permalinkFactory.createPermalink(rootThreadEventSenderId, false) ?: "" + val userLink = permalinkFactory.createPermalink(rootThreadEventSenderId, false) ?: "" val replyFormatted = LocalEchoEventFactory.REPLY_PATTERN.format( permalink, @@ -81,7 +191,7 @@ internal class ThreadsAwarenessHandler @Inject constructor( rootThreadEventBody, body) - val messageTextContent = MessageTextContent( + val messageTextContent = MessageTextContent( msgType = msgType, format = MessageFormat.FORMAT_MATRIX_HTML, body = body, @@ -90,8 +200,40 @@ internal class ThreadsAwarenessHandler @Inject constructor( payload["content"] = messageTextContent - event.mxDecryptionResult = event.mxDecryptionResult?.copy(payload = payload ) + return payload + } + /** + * 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 + } + } + } + + /** + * Try to get the event form the local DB, if the event does not exist null + * will be returned + */ + private fun getEventFromDB(realm: Realm, eventId: String): Event? { + val eventEntity = EventEntity.where(realm, eventId = eventId).findFirst() ?: return null + return EventMapper.map(eventEntity) } private fun isThreadEvent(event: Event): Boolean = From 8ee3f2c6cb66afddb261ec955ea9c6658e2b69e2 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Wed, 3 Nov 2021 11:34:22 +0200 Subject: [PATCH 004/190] Delete ThreadToReplyMapInterceptor Add documentation comments --- .../network/ThreadToReplyMapInterceptor.kt | 105 ------------------ .../handler/room/ThreadsAwarenessHandler.kt | 14 +++ 2 files changed, 14 insertions(+), 105 deletions(-) delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ThreadToReplyMapInterceptor.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ThreadToReplyMapInterceptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ThreadToReplyMapInterceptor.kt deleted file mode 100644 index 519e497e95..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ThreadToReplyMapInterceptor.kt +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (c) 2021 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.network - -import okhttp3.Interceptor -import okhttp3.Request -import okhttp3.Response -import okhttp3.ResponseBody -import okhttp3.ResponseBody.Companion.toResponseBody -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.network.ApiInterceptorListener -import org.matrix.android.sdk.api.network.ApiPath -import org.matrix.android.sdk.internal.di.MatrixScope -import timber.log.Timber -import javax.inject.Inject - -/** - * The goal of this interceptor is to map thread events to be handled as replies. - * The interceptor is responsible for mapping a thread event: - * "m.relates_to":{ - * "event_id":"$eventId", - * "rel_type":"io.element.thread" - * } - * to an equivalent reply event: - * m.relates_to":{ - * "m.in_reply_to":{ - * "event_id":"$eventId" - * } - */ -@MatrixScope -internal class ThreadToReplyMapInterceptor @Inject constructor() : Interceptor { - - init { - Timber.d("MapThreadToReplyInterceptor.init") - } - - override fun intercept(chain: Interceptor.Chain): Response { - val request = chain.request() - - val response = chain.proceed(request) - - if (isSyncRequest(request)) { - Timber.i(" ------> found SYNC REQUEST") - - return response.body?.let { - val contentType = it.contentType() - val rawBody = it.string() - Timber.i(" ------> $rawBody") - - if(rawBody.contains("\"rel_type\":\"io.element.thread\"")){ - Timber.i(" ------> Thread found") - val start = rawBody.indexOf("\"rel_type\":\"io.element.thread\"") - "\"m.relates_to\":{\"event_id\":\"-GoMTnxkfmZczOPvbjcK43WqNib3wiJVaeO_vRxwHIDA\",\"".length +1 - val end = rawBody.indexOf("\"rel_type\":\"io.element.thread\"") + "\"rel_type\":\"io.element.thread\"".length +2 - val substr = rawBody.subSequence(start,end) - val newRaw = rawBody.replaceRange(start,end,"\"m.relates_to\":{\"m.in_reply_to\":{\"event_id\":\"\$HDddlX2bJQmVS0bN5R9HDzcrGDap18b3cFDDYjTjctc\"}},") - Timber.i(" ------> ${substr}") - Timber.i(" ------> new raw $newRaw") - val newBody = newRaw.toResponseBody(contentType) - response.newBuilder().body(newBody).build() - - }else{ - val newBody = rawBody.toResponseBody(contentType) - response.newBuilder().body(newBody).build() - } - } ?: response - -// response.peekBody(Long.MAX_VALUE).string().let { networkResponse -> -// Timber.i(" ------> ThreadToReplyMapInterceptor $networkResponse") -// } - } - -// val path = request.url.encodedPath -// if(path.contains("/sync/")){ -// Timber.i("-----> SYNC REQUEST --> $responseBody") -// -// -// } - -// val body = ResponseBody.create() -// val newResponse = response.newBuilder().body(body) - return response - } - - /** - * Returns true if the request is a sync request, false otherwise - * Example of a sync request: - * https://matrix-client.matrix.org/_matrix/client/r0/sync?filter=0&set_presence=online&t... - */ - private fun isSyncRequest(request: Request): Boolean = - ApiPath.SYNC.method == request.method && request.url.encodedPath.contains(ApiPath.SYNC.path) -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt index 12822aeb1d..6579126140 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt @@ -138,6 +138,9 @@ internal class ThreadsAwarenessHandler @Inject constructor( }) } + /** + * Handle events mainly coming from the RoomSyncHandler + */ fun handleIfNeeded(realm: Realm, roomId: String, event: Event) { @@ -150,6 +153,9 @@ internal class ThreadsAwarenessHandler @Inject constructor( event.mxDecryptionResult = event.mxDecryptionResult?.copy(payload = payload) } + /** + * Handle events while they are being decrypted + */ fun handleIfNeededDuringDecryption(realm: Realm, roomId: String?, event: Event, @@ -236,9 +242,17 @@ internal class ThreadsAwarenessHandler @Inject constructor( return EventMapper.map(eventEntity) } + /** + * Returns True if the event is a thread + * @param event + */ private fun isThreadEvent(event: Event): Boolean = event.content.toModel()?.relatesTo?.type == "io.element.thread" + /** + * Returns the root thread eventId or null otherwise + * @param event + */ private fun getRootThreadEventId(event: Event): String? = event.content.toModel()?.relatesTo?.eventId From 4192c1cf81d055a11f7f23dfa39aca542812b045 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Wed, 3 Nov 2021 11:37:48 +0200 Subject: [PATCH 005/190] Add changelog file --- changelog.d/4246.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/4246.feature diff --git a/changelog.d/4246.feature b/changelog.d/4246.feature new file mode 100644 index 0000000000..6695edf590 --- /dev/null +++ b/changelog.d/4246.feature @@ -0,0 +1 @@ +Make Element Android Thread aware From ec366f1346763f58bce2507ae962d335bb2342c4 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Thu, 4 Nov 2021 12:15:22 +0200 Subject: [PATCH 006/190] PR Remarks --- .../android/sdk/api/session/events/model/RelationType.kt | 4 ++++ .../sdk/internal/session/room/timeline/DefaultTimeline.kt | 2 +- .../session/sync/handler/room/ThreadsAwarenessHandler.kt | 3 ++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt index 7d827f871b..f67efc50ba 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt @@ -28,6 +28,10 @@ object RelationType { /** Lets you define an event which references an existing event.*/ const val REFERENCE = "m.reference" + /** Lets you define an thread event that belongs to another existing event.*/ +// const val THREAD = "m.thread" // m.thread is not yet released in the backend + const val THREAD = "io.element.thread" // io.element.thread will be replaced by m.thread when it is released + /** Lets you define an event which adds a response to an existing event.*/ const val RESPONSE = "org.matrix.response" } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index e9f992f295..2744b5129e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -619,7 +619,7 @@ internal class DefaultTimeline( it?.root }.map { EventMapper.map(it) - }.toList() + } threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(eventEntityList) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt index 6579126140..281b2c1761 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt @@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.RelationType 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 @@ -247,7 +248,7 @@ internal class ThreadsAwarenessHandler @Inject constructor( * @param event */ private fun isThreadEvent(event: Event): Boolean = - event.content.toModel()?.relatesTo?.type == "io.element.thread" + event.content.toModel()?.relatesTo?.type == RelationType.THREAD /** * Returns the root thread eventId or null otherwise From 27d6e271ad73a809983099f5e50314b92cdd2947 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 16 Nov 2021 16:33:04 +0100 Subject: [PATCH 007/190] Add a debug layout to see social login buttons For AS preview only --- .../debug/res/layout/debug_social_login.xml | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 library/ui-styles/src/debug/res/layout/debug_social_login.xml diff --git a/library/ui-styles/src/debug/res/layout/debug_social_login.xml b/library/ui-styles/src/debug/res/layout/debug_social_login.xml new file mode 100644 index 0000000000..895ecddad4 --- /dev/null +++ b/library/ui-styles/src/debug/res/layout/debug_social_login.xml @@ -0,0 +1,113 @@ + + + + + + + +