From a758ad71e665361f707f8b74c19274a904defd1e Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Thu, 10 Mar 2022 17:51:02 +0200 Subject: [PATCH] Add is_falling_back support for rich thread replies Enhance thread awareness handler so normal replies with thread disabled will be visible in te appropriate thread Fix conflicts --- .../sdk/api/session/events/model/Event.kt | 2 +- .../room/model/relation/ReactionInfo.kt | 3 +- .../room/model/relation/RelationContent.kt | 1 + .../model/relation/RelationDefaultContent.kt | 5 +- .../room/model/relation/ReplyToContent.kt | 5 +- .../database/helper/ThreadSummaryHelper.kt | 4 +- .../room/relation/DefaultRelationService.kt | 2 +- .../session/room/relation/EventEditor.kt | 2 +- .../room/send/LocalEchoEventFactory.kt | 16 ++--- .../internal/session/room/send/TextContent.kt | 1 + .../sync/handler/room/RoomSyncHandler.kt | 60 +++++++++---------- .../handler/room/ThreadsAwarenessHandler.kt | 45 ++++++++++---- .../composer/MessageComposerViewModel.kt | 4 +- 13 files changed, 91 insertions(+), 59 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt index 9d86730d9f..817d666cf8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt @@ -389,7 +389,7 @@ fun Event.isReply(): Boolean { } fun Event.isReplyRenderedInThread(): Boolean { - return isReply() && getRelationContent()?.inReplyTo?.shouldRenderInThread() == true + return isReply() && getRelationContent()?.shouldRenderInThread() == true } fun Event.isThread(): Boolean = getRelationContentForType(RelationType.THREAD)?.eventId != null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionInfo.kt index 733d6c37e8..e7bebeeff6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionInfo.kt @@ -26,5 +26,6 @@ data class ReactionInfo( @Json(name = "key") val key: String, // always null for reaction @Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null, - @Json(name = "option") override val option: Int? = null + @Json(name = "option") override val option: Int? = null, + @Json(name = "is_falling_back") override val isFallingBack: Boolean? = null ) : RelationContent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationContent.kt index e2080bb437..f5b3f4c75e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationContent.kt @@ -24,4 +24,5 @@ interface RelationContent { val eventId: String? val inReplyTo: ReplyToContent? val option: Int? + val isFallingBack: Boolean? // Thread fallback to differentiate replies within threads } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt index 10b071a601..5dcb1b4323 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt @@ -23,5 +23,8 @@ data class RelationDefaultContent( @Json(name = "rel_type") override val type: String?, @Json(name = "event_id") override val eventId: String?, @Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null, - @Json(name = "option") override val option: Int? = null + @Json(name = "option") override val option: Int? = null, + @Json(name = "is_falling_back") override val isFallingBack: Boolean? = null ) : RelationContent + +fun RelationDefaultContent.shouldRenderInThread(): Boolean = isFallingBack == false diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReplyToContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReplyToContent.kt index 412a1bfca9..251328bea2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReplyToContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReplyToContent.kt @@ -21,8 +21,5 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class ReplyToContent( - @Json(name = "event_id") val eventId: String? = null, - @Json(name = "render_in") val renderIn: List? = null + @Json(name = "event_id") val eventId: String? = null ) - -fun ReplyToContent.shouldRenderInThread(): Boolean = renderIn?.contains("m.thread") == true diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt index 9ebe1203ad..7087f07162 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt @@ -127,7 +127,7 @@ private fun EventEntity.toTimelineEventEntity(roomMemberContentsByUser: HashMap< return timelineEventEntity } -internal fun ThreadSummaryEntity.Companion.createOrUpdate( +internal suspend fun ThreadSummaryEntity.Companion.createOrUpdate( threadSummaryType: ThreadSummaryUpdateType, realm: Realm, roomId: String, @@ -204,7 +204,7 @@ internal fun ThreadSummaryEntity.Companion.createOrUpdate( } } -private fun decryptIfNeeded(cryptoService: CryptoService?, eventEntity: EventEntity, roomId: String) { +private suspend fun decryptIfNeeded(cryptoService: CryptoService?, eventEntity: EventEntity, roomId: String) { cryptoService ?: return val event = eventEntity.asDomain() if (event.isEncrypted() && event.mxDecryptionResult == null && event.eventId != null) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt index bb2acd8438..ab514d31c8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt @@ -172,7 +172,7 @@ internal class DefaultRelationService @AssistedInject constructor( replyText = replyInThreadText, autoMarkdown = autoMarkdown, rootThreadEventId = rootThreadEventId, - showInThread = true + showInThread = false ) ?.also { saveLocalEcho(it) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt index b54cd71e50..62a910f79d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt @@ -100,7 +100,7 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor: eventReplied = originalTimelineEvent, replyText = newBodyText, autoMarkdown = false, - showInThread = false + showInThread = false // Test that value )?.copy( eventId = replyToEdit.eventId ) ?: return NoOpCancellable diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index ef4c09cb9f..d53c375cbf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -560,7 +560,7 @@ internal class LocalEchoEventFactory @Inject constructor( relatesTo = generateReplyRelationContent( eventId = eventId, rootThreadEventId = rootThreadEventId, - showAsReply = showInThread)) + showInThread = showInThread)) return createMessageEvent(roomId, content) } @@ -570,18 +570,20 @@ internal class LocalEchoEventFactory @Inject constructor( * "m.relates_to": { * "rel_type": "m.thread", * "event_id": "$thread_root", + * "is_falling_back": false, * "m.in_reply_to": { - * "event_id": "$event_target", - * "render_in": ["m.thread"] - * } - * } + * "event_id": "$event_target" + * } + * } */ - private fun generateReplyRelationContent(eventId: String, rootThreadEventId: String? = null, showAsReply: Boolean): RelationDefaultContent = + private fun generateReplyRelationContent(eventId: String, rootThreadEventId: String? = null, showInThread: Boolean): RelationDefaultContent = rootThreadEventId?.let { RelationDefaultContent( type = RelationType.THREAD, eventId = it, - inReplyTo = ReplyToContent(eventId = eventId, renderIn = if (showAsReply) arrayListOf("m.thread") else null)) + isFallingBack = showInThread, + // False when is a rich reply from within a thread, and true when is a reply that should be visible from threads + inReplyTo = ReplyToContent(eventId = eventId)) } ?: RelationDefaultContent(null, null, ReplyToContent(eventId = eventId)) private fun buildFormattedReply(permalink: String, userLink: String, userId: String, bodyFormatted: String, newBodyFormatted: String): String { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/TextContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/TextContent.kt index 65ae90f285..93c0167abe 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/TextContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/TextContent.kt @@ -60,6 +60,7 @@ fun TextContent.toThreadTextContent( relatesTo = RelationDefaultContent( type = RelationType.THREAD, eventId = rootThreadEventId, + isFallingBack = true, inReplyTo = ReplyToContent( eventId = latestThreadEventId )), 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 02855e7ea2..8fe85f0d31 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 @@ -102,11 +102,11 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle data class LEFT(val data: Map) : HandlingStrategy() } - fun handle(realm: Realm, - roomsSyncResponse: RoomsSyncResponse, - isInitialSync: Boolean, - aggregator: SyncResponsePostTreatmentAggregator, - reporter: ProgressReporter? = null) { + suspend fun handle(realm: Realm, + roomsSyncResponse: RoomsSyncResponse, + isInitialSync: Boolean, + aggregator: SyncResponsePostTreatmentAggregator, + reporter: ProgressReporter? = null) { Timber.v("Execute transaction from $this") handleRoomSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), isInitialSync, aggregator, reporter) handleRoomSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), isInitialSync, aggregator, reporter) @@ -121,11 +121,11 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle } // PRIVATE METHODS ***************************************************************************** - private fun handleRoomSync(realm: Realm, - handlingStrategy: HandlingStrategy, - isInitialSync: Boolean, - aggregator: SyncResponsePostTreatmentAggregator, - reporter: ProgressReporter?) { + private suspend fun handleRoomSync(realm: Realm, + handlingStrategy: HandlingStrategy, + isInitialSync: Boolean, + aggregator: SyncResponsePostTreatmentAggregator, + reporter: ProgressReporter?) { val insertType = if (isInitialSync) { EventInsertType.INITIAL_SYNC } else { @@ -158,11 +158,11 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle realm.insertOrUpdate(rooms) } - private fun insertJoinRoomsFromInitSync(realm: Realm, - handlingStrategy: HandlingStrategy.JOINED, - syncLocalTimeStampMillis: Long, - aggregator: SyncResponsePostTreatmentAggregator, - reporter: ProgressReporter?) { + private suspend fun insertJoinRoomsFromInitSync(realm: Realm, + handlingStrategy: HandlingStrategy.JOINED, + syncLocalTimeStampMillis: Long, + aggregator: SyncResponsePostTreatmentAggregator, + reporter: ProgressReporter?) { val bestChunkSize = computeBestChunkSize( listSize = handlingStrategy.data.keys.size, limit = (initialSyncStrategy as? InitialSyncStrategy.Optimized)?.maxRoomsToInsert ?: Int.MAX_VALUE @@ -200,12 +200,12 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle } } - private fun handleJoinedRoom(realm: Realm, - roomId: String, - roomSync: RoomSync, - insertType: EventInsertType, - syncLocalTimestampMillis: Long, - aggregator: SyncResponsePostTreatmentAggregator): RoomEntity { + private suspend fun handleJoinedRoom(realm: Realm, + roomId: String, + roomSync: RoomSync, + insertType: EventInsertType, + syncLocalTimestampMillis: Long, + aggregator: SyncResponsePostTreatmentAggregator): RoomEntity { Timber.v("Handle join sync for room $roomId") val ephemeralResult = (roomSync.ephemeral as? LazyRoomSyncEphemeral.Parsed) @@ -351,15 +351,15 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle return roomEntity } - private fun handleTimelineEvents(realm: Realm, - roomId: String, - roomEntity: RoomEntity, - eventList: List, - prevToken: String? = null, - isLimited: Boolean = true, - insertType: EventInsertType, - syncLocalTimestampMillis: Long, - aggregator: SyncResponsePostTreatmentAggregator): ChunkEntity { + private suspend fun handleTimelineEvents(realm: Realm, + roomId: String, + roomEntity: RoomEntity, + eventList: List, + prevToken: String? = null, + isLimited: Boolean = true, + insertType: EventInsertType, + syncLocalTimestampMillis: Long, + aggregator: SyncResponsePostTreatmentAggregator): ChunkEntity { val lastChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomEntity.roomId) if (isLimited && lastChunk != null) { lastChunk.deleteOnCascade(deleteStateEvents = false, canDeleteRoot = true) 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 23db397ccd..dc0cc52a72 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 @@ -32,6 +32,7 @@ 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.message.MessageType +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent 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 @@ -170,8 +171,9 @@ internal class ThreadsAwarenessHandler @Inject constructor( event.mxDecryptionResult?.payload?.toMutableMap() ?: return null } val eventBody = event.getDecryptedTextSummary() ?: return null + val threadRelation = getRootThreadRelationContent(event) val eventIdToInject = getPreviousEventOrRoot(event) ?: run { - return@makeEventThreadAware injectFallbackIndicator(event, eventBody, eventEntity, eventPayload) + return@makeEventThreadAware injectFallbackIndicator(event, eventBody, eventEntity, eventPayload, threadRelation) } val eventToInject = getEventFromDB(realm, eventIdToInject) val eventToInjectBody = eventToInject?.getDecryptedTextSummary() @@ -183,17 +185,19 @@ internal class ThreadsAwarenessHandler @Inject constructor( roomId = roomId, eventBody = eventBody, eventToInject = eventToInject, - eventToInjectBody = eventToInjectBody) ?: return null + eventToInjectBody = eventToInjectBody, + threadRelation = threadRelation) ?: return null + // update the event contentForNonEncrypted = updateEventEntity(event, eventEntity, eventPayload, messageTextContent) } else { - contentForNonEncrypted = injectFallbackIndicator(event, eventBody, eventEntity, eventPayload) + contentForNonEncrypted = injectFallbackIndicator(event, eventBody, eventEntity, eventPayload, threadRelation) } // 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) + handleEventsThatRelatesTo(realm, roomId, event, eventBody, false, threadRelation) } return contentForNonEncrypted } @@ -205,11 +209,16 @@ internal class ThreadsAwarenessHandler @Inject constructor( * @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? { + 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) + return handleEventsThatRelatesTo(realm, roomId, event, eventBody, true, null) } } return null @@ -224,7 +233,14 @@ internal class ThreadsAwarenessHandler @Inject constructor( * @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? { + private fun handleEventsThatRelatesTo( + realm: Realm, + roomId: String, + event: Event, + eventBody: String, + isFromCache: Boolean, + threadRelation: RelationDefaultContent? + ): String? { event.eventId ?: return null val rootThreadEventId = if (isFromCache) event.eventId else event.getRootThreadEventId() ?: return null eventThatRelatesTo(realm, event.eventId, rootThreadEventId)?.forEach { eventEntityFound -> @@ -236,7 +252,8 @@ internal class ThreadsAwarenessHandler @Inject constructor( roomId = roomId, eventBody = newEventBody, eventToInject = event, - eventToInjectBody = eventBody) ?: return null + eventToInjectBody = eventBody, + threadRelation = threadRelation) ?: return null return updateEventEntity(newEventFound, eventEntityFound, newEventPayload, messageTextContent) } @@ -280,7 +297,9 @@ internal class ThreadsAwarenessHandler @Inject constructor( private fun injectEvent(roomId: String, eventBody: String, eventToInject: Event, - eventToInjectBody: String): Content? { + eventToInjectBody: String, + threadRelation: RelationDefaultContent? + ): Content? { val eventToInjectId = eventToInject.eventId ?: return null val eventIdToInjectSenderId = eventToInject.senderId.orEmpty() val permalink = permalinkFactory.createPermalink(roomId, eventToInjectId, false) @@ -293,6 +312,7 @@ internal class ThreadsAwarenessHandler @Inject constructor( eventBody) return MessageTextContent( + relatesTo = threadRelation, msgType = MessageType.MSGTYPE_TEXT, format = MessageFormat.FORMAT_MATRIX_HTML, body = eventBody, @@ -306,12 +326,14 @@ internal class ThreadsAwarenessHandler @Inject constructor( private fun injectFallbackIndicator(event: Event, eventBody: String, eventEntity: EventEntity?, - eventPayload: MutableMap): String? { + eventPayload: MutableMap, + threadRelation: RelationDefaultContent?): String? { val replyFormatted = LocalEchoEventFactory.QUOTE_PATTERN.format( "In reply to a thread", eventBody) val messageTextContent = MessageTextContent( + relatesTo = threadRelation, msgType = MessageType.MSGTYPE_TEXT, format = MessageFormat.FORMAT_MATRIX_HTML, body = eventBody, @@ -359,6 +381,9 @@ internal class ThreadsAwarenessHandler @Inject constructor( private fun getRootThreadEventId(event: Event): String? = event.content.toModel()?.relatesTo?.eventId + private fun getRootThreadRelationContent(event: Event): RelationDefaultContent? = + event.content.toModel()?.relatesTo + private fun getPreviousEventOrRoot(event: Event): String? = event.content.toModel()?.relatesTo?.inReplyTo?.eventId diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt index 325e9b9330..f7975c9029 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt @@ -465,7 +465,8 @@ class MessageComposerViewModel @AssistedInject constructor( // is original event a reply? val relationContent = state.sendMode.timelineEvent.getRelationContent() val inReplyTo = if (state.rootThreadEventId != null) { - if (relationContent?.inReplyTo?.shouldRenderInThread() == true) { + // Thread event + if (relationContent?.shouldRenderInThread() == true) { // Reply within a thread event relationContent.inReplyTo?.eventId } else { @@ -509,6 +510,7 @@ class MessageComposerViewModel @AssistedInject constructor( is SendMode.Reply -> { val timelineEvent = state.sendMode.timelineEvent val showInThread = state.sendMode.timelineEvent.root.isThread() && state.rootThreadEventId == null + // If threads are disabled this will make the fallback replies visible to clients with threads enabled val rootThreadEventId = if (showInThread) timelineEvent.root.getRootThreadEventId() else null state.rootThreadEventId?.let { room.replyInThread(