From 49b7726ac866ccc7add1c516c8865eb3847dcb14 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Mon, 14 Feb 2022 15:09:01 +0200
Subject: [PATCH 01/38] - Integrate /relations API to create a live thread
 timeline

---
 .../session/room/timeline/ChunkEntityTest.kt  |  24 +-
 .../sdk/api/session/events/model/Event.kt     |   6 +-
 .../session/room/timeline/TimelineEvent.kt    |   1 +
 .../database/RealmSessionStoreMigration.kt    |  14 +-
 .../database/helper/ChunkEntityHelper.kt      |   9 +-
 .../database/helper/ThreadEventsHelper.kt     |   2 +
 .../database/mapper/TimelineEventMapper.kt    |   1 +
 .../internal/database/model/ChunkEntity.kt    |  22 +-
 .../database/model/TimelineEventEntity.kt     |   3 +
 .../database/query/ChunkEntityQueries.kt      |  14 +-
 .../EventAnnotationsSummaryEntityQuery.kt     |   2 +-
 .../EventRelationsAggregationProcessor.kt     |  10 +-
 .../sdk/internal/session/room/RoomAPI.kt      |   2 +
 .../room/relation/DefaultRelationService.kt   |   8 +-
 .../threads/FetchThreadTimelineTask.kt        | 206 ++++++++++++------
 .../session/room/timeline/DefaultTimeline.kt  |   4 +
 .../room/timeline/DefaultTimelineService.kt   |   5 +-
 .../room/timeline/LoadTimelineStrategy.kt     |  58 ++++-
 .../session/room/timeline/TimelineChunk.kt    |  45 +++-
 .../room/timeline/TokenChunkEventPersistor.kt |  24 +-
 .../sync/handler/room/RoomSyncHandler.kt      |  35 ++-
 .../list/viewmodel/ThreadListController.kt    |   2 +-
 22 files changed, 394 insertions(+), 103 deletions(-)

diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt
index 69ae57e644f..5c011c8b2fb 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt
@@ -62,7 +62,11 @@ internal class ChunkEntityTest : InstrumentedTest {
             val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED, System.currentTimeMillis()).let {
                 realm.copyToRealm(it)
             }
-            chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap())
+            chunk.addTimelineEvent(
+                    roomId = ROOM_ID,
+                    eventEntity = fakeEvent,
+                    direction = PaginationDirection.FORWARDS,
+                    roomMemberContentsByUser = emptyMap())
             chunk.timelineEvents.size shouldBeEqualTo 1
         }
     }
@@ -74,8 +78,16 @@ internal class ChunkEntityTest : InstrumentedTest {
             val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED, System.currentTimeMillis()).let {
                 realm.copyToRealm(it)
             }
-            chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap())
-            chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap())
+            chunk.addTimelineEvent(
+                    roomId = ROOM_ID,
+                    eventEntity = fakeEvent,
+                    direction = PaginationDirection.FORWARDS,
+                    roomMemberContentsByUser = emptyMap())
+            chunk.addTimelineEvent(
+                    roomId = ROOM_ID,
+                    eventEntity = fakeEvent,
+                    direction = PaginationDirection.FORWARDS,
+                    roomMemberContentsByUser = emptyMap())
             chunk.timelineEvents.size shouldBeEqualTo 1
         }
     }
@@ -144,7 +156,11 @@ internal class ChunkEntityTest : InstrumentedTest {
             val fakeEvent = event.toEntity(roomId, SendState.SYNCED, System.currentTimeMillis()).let {
                 realm.copyToRealm(it)
             }
-            addTimelineEvent(roomId, fakeEvent, direction, emptyMap())
+            addTimelineEvent(
+                    roomId = roomId,
+                    eventEntity = fakeEvent,
+                    direction = direction,
+                    roomMemberContentsByUser = emptyMap())
         }
     }
 
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 df57ca56811..ed9a0573751 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
@@ -201,7 +201,11 @@ data class Event(
      */
     fun getDecryptedTextSummary(): String? {
         if (isRedacted()) return "Message Deleted"
-        val text = getDecryptedValue() ?: return null
+        val text = getDecryptedValue() ?: run {
+            if (isPoll()) {return getPollQuestion() ?: "created a poll."}
+            return null
+        }
+
         return when {
             isReplyRenderedInThread() || isQuote() -> ContentUtils.extractUsefulTextFromReply(text)
             isFileMessage()                        -> "sent a file."
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt
index 6f8bae876bd..c03d0fd17bc 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt
@@ -54,6 +54,7 @@ data class TimelineEvent(
          * It's not unique on the timeline as it's reset on each chunk.
          */
         val displayIndex: Int,
+        var ownedByThreadChunk: Boolean = false,
         val senderInfo: SenderInfo,
         val annotations: EventAnnotationsSummary? = null,
         val readReceipts: List<ReadReceipt> = emptyList()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
index dfb09155665..056c4b0ceb3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
@@ -57,7 +57,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
 ) : RealmMigration {
 
     companion object {
-        const val SESSION_STORE_SCHEMA_VERSION = 24L
+        const val SESSION_STORE_SCHEMA_VERSION = 26L
     }
 
     /**
@@ -94,6 +94,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
         if (oldVersion <= 21) migrateTo22(realm)
         if (oldVersion <= 22) migrateTo23(realm)
         if (oldVersion <= 23) migrateTo24(realm)
+        if (oldVersion <= 24) migrateTo25(realm)
     }
 
     private fun migrateTo1(realm: DynamicRealm) {
@@ -489,4 +490,15 @@ internal class RealmSessionStoreMigration @Inject constructor(
                 ?.addField(PreviewUrlCacheEntityFields.IMAGE_HEIGHT, Int::class.java)
                 ?.setNullable(PreviewUrlCacheEntityFields.IMAGE_HEIGHT, true)
     }
+
+    private fun migrateTo25(realm: DynamicRealm){
+        Timber.d("Step 24 -> 25")
+        realm.schema.get("ChunkEntity")
+                ?.addField(ChunkEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED)
+                ?.addField(ChunkEntityFields.IS_LAST_FORWARD_THREAD, Boolean::class.java, FieldAttribute.INDEXED)
+
+        realm.schema.get("TimelineEventEntity")
+                ?.addField(TimelineEventEntityFields.OWNED_BY_THREAD_CHUNK, Boolean::class.java)
+
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt
index 289db9fa15b..007017510c3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt
@@ -82,17 +82,18 @@ internal fun ChunkEntity.addStateEvent(roomId: String, stateEvent: EventEntity,
 internal fun ChunkEntity.addTimelineEvent(roomId: String,
                                           eventEntity: EventEntity,
                                           direction: PaginationDirection,
-                                          roomMemberContentsByUser: Map<String, RoomMemberContent?>? = null) {
+                                          ownedByThreadChunk: Boolean = false,
+                                          roomMemberContentsByUser: Map<String, RoomMemberContent?>? = null): TimelineEventEntity? {
     val eventId = eventEntity.eventId
     if (timelineEvents.find(eventId) != null) {
-        return
+        return null
     }
     val displayIndex = nextDisplayIndex(direction)
     val localId = TimelineEventEntity.nextId(realm)
     val senderId = eventEntity.sender ?: ""
 
     // Update RR for the sender of a new message with a dummy one
-    val readReceiptsSummaryEntity = handleReadReceipts(realm, roomId, eventEntity, senderId)
+    val readReceiptsSummaryEntity = if (!ownedByThreadChunk) handleReadReceipts(realm, roomId, eventEntity, senderId) else null
     val timelineEventEntity = realm.createObject<TimelineEventEntity>().apply {
         this.localId = localId
         this.root = eventEntity
@@ -102,6 +103,7 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String,
                 ?.also { it.cleanUp(eventEntity.sender) }
         this.readReceipts = readReceiptsSummaryEntity
         this.displayIndex = displayIndex
+        this.ownedByThreadChunk = ownedByThreadChunk
         val roomMemberContent = roomMemberContentsByUser?.get(senderId)
         this.senderAvatar = roomMemberContent?.avatarUrl
         this.senderName = roomMemberContent?.displayName
@@ -113,6 +115,7 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String,
     }
     // numberOfTimelineEvents++
     timelineEvents.add(timelineEventEntity)
+    return timelineEventEntity
 }
 
 private fun computeIsUnique(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
index f703bfaf82a..7f6b64da75d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
@@ -98,6 +98,7 @@ internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId:
     val messages = TimelineEventEntity
             .whereRoomId(realm, roomId = roomId)
             .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId)
+            .distinct(TimelineEventEntityFields.ROOT.EVENT_ID)
             .count()
             .toInt()
 
@@ -156,6 +157,7 @@ internal fun TimelineEventEntity.Companion.findAllThreadsForRoomId(realm: Realm,
         TimelineEventEntity
                 .whereRoomId(realm, roomId = roomId)
                 .equalTo(TimelineEventEntityFields.ROOT.IS_ROOT_THREAD, true)
+                .equalTo(TimelineEventEntityFields.OWNED_BY_THREAD_CHUNK, false)
                 .sort("${TimelineEventEntityFields.ROOT.THREAD_SUMMARY_LATEST_MESSAGE}.${TimelineEventEntityFields.ROOT.ORIGIN_SERVER_TS}", Sort.DESCENDING)
 
 /**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/TimelineEventMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/TimelineEventMapper.kt
index f3bea68c26e..1020fa33da7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/TimelineEventMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/TimelineEventMapper.kt
@@ -46,6 +46,7 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS
                         isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName,
                         avatarUrl = timelineEventEntity.senderAvatar
                 ),
+                ownedByThreadChunk = timelineEventEntity.ownedByThreadChunk,
                 readReceipts = readReceipts
                         ?.distinctBy {
                             it.user
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt
index c45c27ed08a..8e4d6fc9163 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt
@@ -23,6 +23,7 @@ import io.realm.annotations.Index
 import io.realm.annotations.LinkingObjects
 import org.matrix.android.sdk.internal.extensions.assertIsManaged
 import org.matrix.android.sdk.internal.extensions.clearWith
+import timber.log.Timber
 
 internal open class ChunkEntity(@Index var prevToken: String? = null,
         // Because of gaps we can have several chunks with nextToken == null
@@ -33,7 +34,10 @@ internal open class ChunkEntity(@Index var prevToken: String? = null,
                                 var timelineEvents: RealmList<TimelineEventEntity> = RealmList(),
         // Only one chunk will have isLastForward == true
                                 @Index var isLastForward: Boolean = false,
-                                @Index var isLastBackward: Boolean = false
+                                @Index var isLastBackward: Boolean = false,
+        // Threads
+                                @Index var rootThreadEventId: String? = null,
+                                @Index var isLastForwardThread: Boolean = false,
 ) : RealmObject() {
 
     fun identifier() = "${prevToken}_$nextToken"
@@ -58,3 +62,19 @@ internal fun ChunkEntity.deleteOnCascade(deleteStateEvents: Boolean, canDeleteRo
     }
     deleteFromRealm()
 }
+
+/**
+ * Delete the chunk along with the thread events that were temporarily created
+ */
+internal fun ChunkEntity.deleteAndClearThreadEvents() {
+    assertIsManaged()
+    timelineEvents
+            .filter { it.ownedByThreadChunk }
+            .forEach {
+                it.deleteOnCascade(false)
+            }
+    deleteFromRealm()
+}
+
+
+
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt
index 185f0e2dcc4..aacd6570bc4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt
@@ -32,6 +32,9 @@ internal open class TimelineEventEntity(var localId: Long = 0,
                                         var isUniqueDisplayName: Boolean = false,
                                         var senderAvatar: String? = null,
                                         var senderMembershipEventId: String? = null,
+                                        // ownedByThreadChunk indicates that the current TimelineEventEntity belongs
+                                        // to a thread chunk and is a temporarily event.
+                                        var ownedByThreadChunk: Boolean = false,
                                         var readReceipts: ReadReceiptsSummaryEntity? = null
 ) : RealmObject() {
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt
index 156a8dd767c..ece46555a7e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt
@@ -45,10 +45,22 @@ internal fun ChunkEntity.Companion.findLastForwardChunkOfRoom(realm: Realm, room
             .equalTo(ChunkEntityFields.IS_LAST_FORWARD, true)
             .findFirst()
 }
-
+internal fun ChunkEntity.Companion.findLastForwardChunkOfThread(realm: Realm, roomId: String, rootThreadEventId: String): ChunkEntity? {
+    return where(realm, roomId)
+            .equalTo(ChunkEntityFields.ROOT_THREAD_EVENT_ID, rootThreadEventId)
+            .equalTo(ChunkEntityFields.IS_LAST_FORWARD_THREAD, true)
+            .findFirst()
+}
+internal fun ChunkEntity.Companion.findEventInThreadChunk(realm: Realm, roomId: String, event: String): ChunkEntity? {
+    return where(realm, roomId)
+            .`in`(ChunkEntityFields.TIMELINE_EVENTS.EVENT_ID, arrayListOf(event).toTypedArray())
+            .equalTo(ChunkEntityFields.IS_LAST_FORWARD_THREAD, true)
+            .findFirst()
+}
 internal fun ChunkEntity.Companion.findAllIncludingEvents(realm: Realm, eventIds: List<String>): RealmResults<ChunkEntity> {
     return realm.where<ChunkEntity>()
             .`in`(ChunkEntityFields.TIMELINE_EVENTS.EVENT_ID, eventIds.toTypedArray())
+            .isNull(ChunkEntityFields.ROOT_THREAD_EVENT_ID)
             .findAll()
 }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt
index 14cb7e22da2..6caa832110c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt
@@ -34,7 +34,7 @@ internal fun EventAnnotationsSummaryEntity.Companion.create(realm: Realm, roomId
         this.roomId = roomId
     }
     // Denormalization
-    TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId).findFirst()?.let {
+    TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId).findAll()?.forEach {
         it.annotations = obj
     }
     return obj
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
index acceaf6e24c..2eebb70bdcf 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
@@ -57,6 +57,7 @@ import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryE
 import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryEntityFields
 import org.matrix.android.sdk.internal.database.model.ReferencesAggregatedSummaryEntity
 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.create
 import org.matrix.android.sdk.internal.database.query.getOrCreate
 import org.matrix.android.sdk.internal.database.query.where
@@ -117,8 +118,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
 
                         EventAnnotationsSummaryEntity.where(realm, roomId, event.eventId ?: "").findFirst()
                                 ?.let {
-                                    TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId ?: "").findFirst()
-                                            ?.let { tet -> tet.annotations = it }
+                                    TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId ?: "").findAll()
+                                            ?.forEach { tet -> tet.annotations = it }
                                 }
                     }
 
@@ -335,7 +336,10 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
         }
 
         if (!isLocalEcho) {
-            val replaceEvent = TimelineEventEntity.where(realm, roomId, eventId).findFirst()
+            val replaceEvent = TimelineEventEntity
+                    .where(realm, roomId, eventId)
+                    .equalTo(TimelineEventEntityFields.OWNED_BY_THREAD_CHUNK, false)
+                    .findFirst()
             handleThreadSummaryEdition(editedEvent, replaceEvent, existingSummary?.editions)
         }
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
index 399bfbd0e45..86929e013f3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
@@ -227,6 +227,8 @@ internal interface RoomAPI {
                              @Path("eventId") eventId: String,
                              @Path("relationType") relationType: String,
                              @Path("eventType") eventType: String,
+                             @Query("from") from: String? = null,
+                             @Query("to") to: String? = null,
                              @Query("limit") limit: Int? = null
     ): RelationsResponse
 
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 3abf28fdd4f..d22583e8b7c 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
@@ -206,7 +206,13 @@ internal class DefaultRelationService @AssistedInject constructor(
     }
 
     override suspend fun fetchThreadTimeline(rootThreadEventId: String): Boolean {
-        return fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params(roomId, rootThreadEventId))
+        fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params(
+                roomId,
+                rootThreadEventId,
+                null,
+                10
+        ))
+        return true
     }
 
     /**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
index e0d501c515f..6b071fbd6e3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2021 The Matrix.org Foundation C.I.C.
+ * Copyright 2022 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.
@@ -27,7 +27,6 @@ import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
 import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
 import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
 import org.matrix.android.sdk.internal.database.helper.addTimelineEvent
-import org.matrix.android.sdk.internal.database.helper.updateThreadSummaryIfNeeded
 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.ChunkEntity
@@ -36,8 +35,10 @@ import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEnt
 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.ReactionAggregatedSummaryEntity
+import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
 import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
-import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
+import org.matrix.android.sdk.internal.database.query.find
+import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfThread
 import org.matrix.android.sdk.internal.database.query.getOrCreate
 import org.matrix.android.sdk.internal.database.query.getOrNull
 import org.matrix.android.sdk.internal.database.query.where
@@ -47,16 +48,39 @@ import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
 import org.matrix.android.sdk.internal.network.executeRequest
 import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent
 import org.matrix.android.sdk.internal.session.room.RoomAPI
+import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse
 import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
 import org.matrix.android.sdk.internal.task.Task
 import org.matrix.android.sdk.internal.util.awaitTransaction
 import timber.log.Timber
 import javax.inject.Inject
 
-internal interface FetchThreadTimelineTask : Task<FetchThreadTimelineTask.Params, Boolean> {
+/***
+ * This class is responsible to Fetch paginated chunks of the thread timeline using the /relations API
+ *
+ *
+ * How it works
+ *
+ * The problem?
+ *  - We cannot use the existing timeline architecture to paginate through the timeline
+ *  - We want our new events to be live, so any interactions with them like reactions will continue to work. We should
+ *    handle appropriately the existing events from /messages api with the new events from /relations.
+ *  - Handling edge cases like receiving an event from /messages while you have already created a new one from the /relations response
+ *
+ * The solution
+ * We generate a temporarily thread chunk that will be used to store any new paginated results from the /relations api
+ * We bind the timeline events from that chunk with the already existing ones. So we will have one common instance, and
+ * all reactions, edits etc will continue to work. If the events do not exists we create them
+ * and we will reuse the same EventEntity instance when (and if) the same event will be fetched from the main (/messages) timeline
+ *
+ */
+internal interface FetchThreadTimelineTask : Task<FetchThreadTimelineTask.Params, DefaultFetchThreadTimelineTask.Result> {
     data class Params(
             val roomId: String,
-            val rootThreadEventId: String
+            val rootThreadEventId: String,
+            val from: String?,
+            val limit: Int
+
     )
 }
 
@@ -69,93 +93,133 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor(
         private val cryptoService: DefaultCryptoService
 ) : FetchThreadTimelineTask {
 
-    override suspend fun execute(params: FetchThreadTimelineTask.Params): Boolean {
+    enum class Result {
+        SHOULD_FETCH_MORE,
+        REACHED_END,
+        SUCCESS
+    }
+
+    override suspend fun execute(params: FetchThreadTimelineTask.Params): Result {
         val isRoomEncrypted = cryptoSessionInfoProvider.isRoomEncrypted(params.roomId)
         val response = executeRequest(globalErrorReceiver) {
             roomAPI.getRelations(
                     roomId = params.roomId,
                     eventId = params.rootThreadEventId,
                     relationType = RelationType.IO_THREAD,
+                    from = params.from,
                     eventType = if (isRoomEncrypted) EventType.ENCRYPTED else EventType.MESSAGE,
-                    limit = 2000
+                    limit = params.limit
             )
         }
 
-        val threadList = response.chunks + listOfNotNull(response.originalEvent)
-
-        return storeNewEventsIfNeeded(threadList, params.roomId)
+        Timber.i("###THREADS FetchThreadTimelineTask Fetched size:${response.chunks.size} nextBatch:${response.nextBatch} ")
+        return handleRelationsResponse(response, params)
     }
 
-    /**
-     * Store new events if they are not already received, and returns weather or not,
-     * a timeline update should be made
-     * @param threadList is the list containing the thread replies
-     * @param roomId the roomId of the the thread
-     * @return
-     */
-    private suspend fun storeNewEventsIfNeeded(threadList: List<Event>, roomId: String): Boolean {
-        var eventsSkipped = 0
-        monarchy
-                .awaitTransaction { realm ->
-                    val chunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)
-
-                    val optimizedThreadSummaryMap = hashMapOf<String, EventEntity>()
-                    val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
-
-                    for (event in threadList.reversed()) {
-                        if (event.eventId == null || event.senderId == null || event.type == null) {
-                            eventsSkipped++
-                            continue
-                        }
-
-                        if (EventEntity.where(realm, event.eventId).findFirst() != null) {
-                            //  Skip if event already exists
-                            eventsSkipped++
-                            continue
-                        }
-                        if (event.isEncrypted()) {
-                            // Decrypt events that will be stored
-                            decryptIfNeeded(event, roomId)
-                        }
-
-                        handleReaction(realm, event, roomId)
-
-                        val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
-                        val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC)
-
-                        // Sender info
-                        roomMemberContentsByUser.getOrPut(event.senderId) {
-                            // If we don't have any new state on this user, get it from db
-                            val rootStateEvent = CurrentStateEventEntity.getOrNull(realm, roomId, event.senderId, EventType.STATE_ROOM_MEMBER)?.root
-                            rootStateEvent?.asDomain()?.getFixedRoomMemberContent()
-                        }
-
-                        chunk?.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser)
-                        eventEntity.rootThreadEventId?.let {
-                            // This is a thread event
-                            optimizedThreadSummaryMap[it] = eventEntity
-                        } ?: run {
-                            // This is a normal event or a root thread one
-                            optimizedThreadSummaryMap[eventEntity.eventId] = eventEntity
-                        }
+    private suspend fun handleRelationsResponse(response: RelationsResponse,
+                                                params: FetchThreadTimelineTask.Params): Result {
+
+        val threadList = response.chunks
+        val threadRootEvent = response.originalEvent
+        val hasReachEnd = response.nextBatch == null
+
+        monarchy.awaitTransaction { realm ->
+
+            val threadChunk = ChunkEntity.findLastForwardChunkOfThread(realm, params.roomId, params.rootThreadEventId)
+                    ?: run {
+                        return@awaitTransaction
                     }
 
-                    optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(
-                            roomId = roomId,
-                            realm = realm,
-                            currentUserId = userId,
-                            shouldUpdateNotifications = false
-                    )
+            threadChunk.prevToken = response.nextBatch
+            val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
+
+            for (event in threadList) {
+                if (event.eventId == null || event.senderId == null || event.type == null) {
+                    continue
+                }
+
+                if (threadChunk.timelineEvents.find(event.eventId) != null) {
+                    // Event already exists in thread chunk, skip it
+                    Timber.i("###THREADS FetchThreadTimelineTask event: ${event.eventId} already exists in thread chunk, skip it")
+                    continue
+                }
+
+                val timelineEvent = TimelineEventEntity
+                        .where(realm, roomId = params.roomId, event.eventId)
+                        .findFirst()
+
+                if (timelineEvent != null) {
+                    // Event already exists but not in the thread chunk
+                    // Lets added there
+                    Timber.i("###THREADS FetchThreadTimelineTask event: ${event.eventId} exists but not in the thread chunk, add it at the end")
+                    threadChunk.timelineEvents.add(timelineEvent)
+                } else {
+                    Timber.i("###THREADS FetchThreadTimelineTask event: ${event.eventId} is brand NEW create an entity and add it!")
+                    val eventEntity = createEventEntity(params.roomId, event, realm)
+                    roomMemberContentsByUser.addSenderState(realm, params.roomId, event.senderId)
+                    threadChunk.addTimelineEvent(
+                            roomId = params.roomId,
+                            eventEntity = eventEntity,
+                            direction = PaginationDirection.FORWARDS,
+                            ownedByThreadChunk = true,
+                            roomMemberContentsByUser = roomMemberContentsByUser)
+                }
+            }
+
+            if (hasReachEnd) {
+                val rootThread = TimelineEventEntity
+                        .where(realm, roomId = params.roomId, params.rootThreadEventId)
+                        .findFirst()
+                if (rootThread != null) {
+                    // If root thread event already exists add it to our chunk
+                    threadChunk.timelineEvents.add(rootThread)
+                    Timber.i("###THREADS FetchThreadTimelineTask root thread event: ${params.rootThreadEventId} found and added!")
+                } else if (threadRootEvent?.senderId != null) {
+                    // Case when thread event is not in the device
+                    Timber.i("###THREADS FetchThreadTimelineTask root thread event: ${params.rootThreadEventId} NOT FOUND! Lets create a temp one")
+                    val eventEntity = createEventEntity(params.roomId, threadRootEvent, realm)
+                    roomMemberContentsByUser.addSenderState(realm, params.roomId, threadRootEvent.senderId)
+                    threadChunk.addTimelineEvent(
+                            roomId = params.roomId,
+                            eventEntity = eventEntity,
+                            direction = PaginationDirection.FORWARDS,
+                            ownedByThreadChunk = true,
+                            roomMemberContentsByUser = roomMemberContentsByUser)
                 }
-        Timber.i("----> size: ${threadList.size} | skipped: $eventsSkipped | threads: ${threadList.map { it.eventId }}")
+            }
+        }
 
-        return eventsSkipped == threadList.size
+        return if (hasReachEnd) {
+            Result.REACHED_END
+        } else {
+            Result.SHOULD_FETCH_MORE
+        }
     }
 
+    // TODO Reuse this function to all the app
     /**
-     * Invoke the event decryption mechanism for a specific event
+     * If we don't have any new state on this user, get it from db
      */
+    private fun HashMap<String, RoomMemberContent?>.addSenderState(realm: Realm, roomId: String, senderId: String) {
+        getOrPut(senderId) {
+            CurrentStateEventEntity
+                    .getOrNull(realm, roomId, senderId, EventType.STATE_ROOM_MEMBER)
+                    ?.root?.asDomain()
+                    ?.getFixedRoomMemberContent()
+        }
+    }
 
+    /**
+     * Create an EventEntity to be added in the TimelineEventEntity
+     */
+    private fun createEventEntity(roomId: String, event: Event, realm: Realm): EventEntity {
+        val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
+        return event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC)
+    }
+
+    /**
+     * Invoke the event decryption mechanism for a specific event
+     */
     private fun decryptIfNeeded(event: Event, roomId: String) {
         try {
             // Event from sync does not have roomId, so add it to the event first
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 3dd4225b2c3..56629866634 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
@@ -38,6 +38,7 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
 import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
 import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
 import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
+import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
 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.SemaphoreCoroutineSequencer
@@ -58,6 +59,7 @@ internal class DefaultTimeline(private val roomId: String,
                                paginationTask: PaginationTask,
                                getEventTask: GetContextOfEventTask,
                                fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
+                               fetchThreadTimelineTask: FetchThreadTimelineTask,
                                timelineEventMapper: TimelineEventMapper,
                                timelineInput: TimelineInput,
                                threadsAwarenessHandler: ThreadsAwarenessHandler,
@@ -89,7 +91,9 @@ internal class DefaultTimeline(private val roomId: String,
             realm = backgroundRealm,
             eventDecryptor = eventDecryptor,
             paginationTask = paginationTask,
+            realmConfiguration = realmConfiguration,
             fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
+            fetchThreadTimelineTask = fetchThreadTimelineTask,
             getContextOfEventTask = getEventTask,
             timelineInput = timelineInput,
             timelineEventMapper = timelineEventMapper,
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 d7d61f0b478..df552b6178c 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
@@ -40,6 +40,7 @@ import org.matrix.android.sdk.internal.database.query.where
 import org.matrix.android.sdk.internal.di.SessionDatabase
 import org.matrix.android.sdk.internal.di.UserId
 import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
+import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
 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
@@ -55,6 +56,7 @@ internal class DefaultTimelineService @AssistedInject constructor(
         private val eventDecryptor: TimelineEventDecryptor,
         private val paginationTask: PaginationTask,
         private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
+        private val fetchThreadTimelineTask: FetchThreadTimelineTask,
         private val timelineEventMapper: TimelineEventMapper,
         private val loadRoomMembersTask: LoadRoomMembersTask,
         private val threadsAwarenessHandler: ThreadsAwarenessHandler,
@@ -76,10 +78,11 @@ internal class DefaultTimelineService @AssistedInject constructor(
                 realmConfiguration = monarchy.realmConfiguration,
                 coroutineDispatchers = coroutineDispatchers,
                 paginationTask = paginationTask,
+                fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
                 timelineEventMapper = timelineEventMapper,
                 timelineInput = timelineInput,
                 eventDecryptor = eventDecryptor,
-                fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
+                fetchThreadTimelineTask = fetchThreadTimelineTask,
                 loadRoomMembersTask = loadRoomMembersTask,
                 readReceiptHandler = readReceiptHandler,
                 getEventTask = contextOfEventTask,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
index f332c4a35f6..867589ccc07 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
@@ -19,20 +19,28 @@ package org.matrix.android.sdk.internal.session.room.timeline
 import io.realm.OrderedCollectionChangeSet
 import io.realm.OrderedRealmCollectionChangeListener
 import io.realm.Realm
+import io.realm.RealmConfiguration
 import io.realm.RealmResults
+import io.realm.kotlin.createObject
 import kotlinx.coroutines.CompletableDeferred
 import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.api.session.room.timeline.Timeline
 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.internal.database.helper.addIfNecessary
 import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
 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.ChunkEntityFields
+import org.matrix.android.sdk.internal.database.model.RoomEntity
+import org.matrix.android.sdk.internal.database.model.deleteAndClearThreadEvents
 import org.matrix.android.sdk.internal.database.query.findAllIncludingEvents
+import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfThread
 import org.matrix.android.sdk.internal.database.query.where
+import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
 import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
+import timber.log.Timber
 import java.util.concurrent.atomic.AtomicReference
 
 /**
@@ -76,6 +84,8 @@ internal class LoadTimelineStrategy(
             val realm: AtomicReference<Realm>,
             val eventDecryptor: TimelineEventDecryptor,
             val paginationTask: PaginationTask,
+            val realmConfiguration: RealmConfiguration,
+            val fetchThreadTimelineTask: FetchThreadTimelineTask,
             val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
             val getContextOfEventTask: GetContextOfEventTask,
             val timelineInput: TimelineInput,
@@ -90,7 +100,6 @@ internal class LoadTimelineStrategy(
     private var getContextLatch: CompletableDeferred<Unit>? = null
     private var chunkEntity: RealmResults<ChunkEntity>? = null
     private var timelineChunk: TimelineChunk? = null
-
     private val chunkEntityListener = OrderedRealmCollectionChangeListener { _: RealmResults<ChunkEntity>, changeSet: OrderedCollectionChangeSet ->
         // Can be call either when you open a permalink on an unknown event
         // or when there is a gap in the timeline.
@@ -170,6 +179,9 @@ internal class LoadTimelineStrategy(
         getContextLatch?.cancel()
         chunkEntity = null
         timelineChunk = null
+        if(mode is Mode.Thread) {
+            clearThreadChunkEntity(dependencies.realm.get(), mode.rootThreadEventId)
+        }
     }
 
     suspend fun loadMore(count: Int, direction: Timeline.Direction, fetchOnServerIfNeeded: Boolean = true): LoadMoreResult {
@@ -185,6 +197,9 @@ internal class LoadTimelineStrategy(
                 return LoadMoreResult.FAILURE
             }
         }
+        if (mode is Mode.Thread) {
+            return timelineChunk?.loadMoreThread(count, Timeline.Direction.BACKWARDS) ?: LoadMoreResult.FAILURE
+        }
         return timelineChunk?.loadMore(count, direction, fetchOnServerIfNeeded) ?: LoadMoreResult.FAILURE
     }
 
@@ -201,7 +216,7 @@ internal class LoadTimelineStrategy(
     }
 
     private fun buildSendingEvents(): List<TimelineEvent> {
-        return if (hasReachedLastForward()) {
+        return if (hasReachedLastForward() || mode is Mode.Thread) {
             sendingEventsDataSource.buildSendingEvents()
         } else {
             emptyList()
@@ -219,13 +234,48 @@ internal class LoadTimelineStrategy(
                 ChunkEntity.findAllIncludingEvents(realm, listOf(mode.originEventId))
             }
             is Mode.Thread    -> {
+                recreateThreadChunkEntity(realm, mode.rootThreadEventId)
                 ChunkEntity.where(realm, roomId)
-                        .equalTo(ChunkEntityFields.IS_LAST_FORWARD, true)
+                        .equalTo(ChunkEntityFields.ROOT_THREAD_EVENT_ID, mode.rootThreadEventId)
+                        .equalTo(ChunkEntityFields.IS_LAST_FORWARD_THREAD, true)
                         .findAll()
             }
         }
     }
 
+    /**
+     * Clear any existing thread chunk entity and create a new one, with the
+     * rootThreadEventId included
+     */
+    private fun recreateThreadChunkEntity(realm: Realm, rootThreadEventId: String) {
+        realm.executeTransaction {
+            // Lets delete the chunk and start a new one
+            ChunkEntity.findLastForwardChunkOfThread(it, roomId, rootThreadEventId)?.deleteAndClearThreadEvents()?.let {
+                Timber.i("###THREADS LoadTimelineStrategy [onStart] thread chunk cleared..")
+            }
+            val threadChunk = it.createObject<ChunkEntity>().apply {
+                Timber.i("###THREADS LoadTimelineStrategy [onStart] Created new thread chunk with rootThreadEventId: $rootThreadEventId")
+                this.rootThreadEventId = rootThreadEventId
+                this.isLastForwardThread = true
+            }
+            if (threadChunk.isValid) {
+                RoomEntity.where(it, roomId).findFirst()?.addIfNecessary(threadChunk)
+            }
+        }
+    }
+
+    /**
+     * Clear any existing thread chunk
+     */
+    private fun clearThreadChunkEntity(realm: Realm, rootThreadEventId: String) {
+        realm.executeTransaction {
+            ChunkEntity.findLastForwardChunkOfThread(it, roomId, rootThreadEventId)?.deleteAndClearThreadEvents()?.let {
+                Timber.i("###THREADS LoadTimelineStrategy [onStop] thread chunk cleared..")
+            }
+
+        }
+    }
+
     private fun hasReachedLastForward(): Boolean {
         return timelineChunk?.hasReachedLastForward().orFalse()
     }
@@ -237,8 +287,10 @@ internal class LoadTimelineStrategy(
                     timelineSettings = dependencies.timelineSettings,
                     roomId = roomId,
                     timelineId = timelineId,
+                    fetchThreadTimelineTask = dependencies.fetchThreadTimelineTask,
                     eventDecryptor = dependencies.eventDecryptor,
                     paginationTask = dependencies.paginationTask,
+                    realmConfiguration = dependencies.realmConfiguration,
                     fetchTokenAndPaginateTask = dependencies.fetchTokenAndPaginateTask,
                     timelineEventMapper = dependencies.timelineEventMapper,
                     uiEchoManager = uiEchoManager,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
index 8507b63d1f7..7f6e5b6c7c9 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.timeline
 
 import io.realm.OrderedCollectionChangeSet
 import io.realm.OrderedRealmCollectionChangeListener
+import io.realm.RealmConfiguration
 import io.realm.RealmObjectChangeListener
 import io.realm.RealmQuery
 import io.realm.RealmResults
@@ -36,6 +37,8 @@ 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.session.room.relation.threads.DefaultFetchThreadTimelineTask
+import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
 import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
 import timber.log.Timber
 import java.util.Collections
@@ -50,8 +53,10 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
                              private val timelineSettings: TimelineSettings,
                              private val roomId: String,
                              private val timelineId: String,
+                             private val fetchThreadTimelineTask: FetchThreadTimelineTask,
                              private val eventDecryptor: TimelineEventDecryptor,
                              private val paginationTask: PaginationTask,
+                             private val realmConfiguration: RealmConfiguration,
                              private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
                              private val timelineEventMapper: TimelineEventMapper,
                              private val uiEchoManager: UIEchoManager? = null,
@@ -142,6 +147,9 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
         val loadFromStorage = loadFromStorage(count, direction).also {
             logLoadedFromStorage(it, direction)
         }
+        if (loadFromStorage.numberOfEvents == 6) {
+            Timber.i("here")
+        }
 
         val offsetCount = count - loadFromStorage.numberOfEvents
 
@@ -158,6 +166,29 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
         }
     }
 
+    /**
+     * This function will fetch more live thread timeline events using the /relations api. It will
+     * always fetch results, while we want our data to be up to dated.
+     */
+    suspend fun loadMoreThread(count: Int, direction: Timeline.Direction): LoadMoreResult {
+
+        return if (direction == Timeline.Direction.BACKWARDS) {
+            try {
+                fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params(
+                        roomId,
+                        timelineSettings.rootThreadEventId!!,
+                        chunkEntity.prevToken,
+                        count
+                )).toLoadMoreResult()
+            } catch (failure: Throwable) {
+                Timber.e(failure, "Failed to fetch thread timeline events from the server")
+                LoadMoreResult.FAILURE
+            }
+        } else {
+            LoadMoreResult.FAILURE
+        }
+    }
+
     private suspend fun delegateLoadMore(fetchFromServerIfNeeded: Boolean, offsetCount: Int, direction: Timeline.Direction): LoadMoreResult {
         return if (direction == Timeline.Direction.FORWARDS) {
             val nextChunkEntity = chunkEntity.nextChunk
@@ -287,7 +318,7 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
      * @return the number of events loaded. If we are in a thread timeline it also returns
      * whether or not we reached the end/root message
      */
-    private suspend fun loadFromStorage(count: Int, direction: Timeline.Direction): LoadedFromStorage {
+    private fun loadFromStorage(count: Int, direction: Timeline.Direction): LoadedFromStorage {
         val displayIndex = getNextDisplayIndex(direction) ?: return LoadedFromStorage()
         val baseQuery = timelineEventEntities.where()
 
@@ -414,6 +445,14 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
         }
     }
 
+    private fun DefaultFetchThreadTimelineTask.Result.toLoadMoreResult(): LoadMoreResult {
+        return when (this) {
+            DefaultFetchThreadTimelineTask.Result.REACHED_END -> LoadMoreResult.REACHED_END
+            DefaultFetchThreadTimelineTask.Result.SHOULD_FETCH_MORE,
+            DefaultFetchThreadTimelineTask.Result.SUCCESS     -> LoadMoreResult.SUCCESS
+        }
+    }
+
     private fun getOffsetIndex(): Int {
         var offset = 0
         var currentNextChunk = nextChunk
@@ -455,6 +494,7 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
                 }
             }
         }
+
         if (insertions.isNotEmpty() || modifications.isNotEmpty()) {
             onBuiltEvents(true)
         }
@@ -489,6 +529,8 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
                 timelineId = timelineId,
                 eventDecryptor = eventDecryptor,
                 paginationTask = paginationTask,
+                realmConfiguration = realmConfiguration,
+                fetchThreadTimelineTask = fetchThreadTimelineTask,
                 fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
                 timelineEventMapper = timelineEventMapper,
                 uiEchoManager = uiEchoManager,
@@ -535,7 +577,6 @@ private fun ChunkEntity.sortedTimelineEvents(rootThreadEventId: String?): RealmR
                 .or()
                 .equalTo(TimelineEventEntityFields.ROOT.EVENT_ID, rootThreadEventId)
                 .endGroup()
-                .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
                 .findAll()
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt
index 6607e71bd9c..63383a99b3c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt
@@ -34,6 +34,7 @@ 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.RoomEntity
 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.copyToRealmOrIgnore
 import org.matrix.android.sdk.internal.database.query.create
 import org.matrix.android.sdk.internal.database.query.find
@@ -49,10 +50,10 @@ import javax.inject.Inject
  * Insert Chunk in DB, and eventually link next and previous chunk in db.
  */
 internal class TokenChunkEventPersistor @Inject constructor(
-                                                            @SessionDatabase private val monarchy: Monarchy,
-                                                            @UserId private val userId: String,
-                                                            private val lightweightSettingsStorage: LightweightSettingsStorage,
-                                                            private val liveEventManager: Lazy<StreamEventsManager>) {
+        @SessionDatabase private val monarchy: Monarchy,
+        @UserId private val userId: String,
+        private val lightweightSettingsStorage: LightweightSettingsStorage,
+        private val liveEventManager: Lazy<StreamEventsManager>) {
 
     enum class Result {
         SHOULD_FETCH_MORE,
@@ -145,9 +146,12 @@ internal class TokenChunkEventPersistor @Inject constructor(
                 if (event.eventId == null || event.senderId == null) {
                     return@forEach
                 }
-                // We check for the timeline event with this id
+                // We check for the timeline event with this id, but not in the thread chunk
                 val eventId = event.eventId
-                val existingTimelineEvent = TimelineEventEntity.where(realm, roomId, eventId).findFirst()
+                val existingTimelineEvent = TimelineEventEntity
+                        .where(realm, roomId, eventId)
+                        .equalTo(TimelineEventEntityFields.OWNED_BY_THREAD_CHUNK, false)
+                        .findFirst()
                 // If it exists, we want to stop here, just link the prevChunk
                 val existingChunk = existingTimelineEvent?.chunk?.firstOrNull()
                 if (existingChunk != null) {
@@ -173,7 +177,7 @@ internal class TokenChunkEventPersistor @Inject constructor(
                     return@processTimelineEvents
                 }
                 val ageLocalTs = event.unsignedData?.age?.let { now - it }
-                val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
+                var eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
                 if (event.type == EventType.STATE_ROOM_MEMBER && event.stateKey != null) {
                     val contentToUse = if (direction == PaginationDirection.BACKWARDS) {
                         event.prevContent
@@ -183,7 +187,11 @@ internal class TokenChunkEventPersistor @Inject constructor(
                     roomMemberContentsByUser[event.stateKey] = contentToUse.toModel<RoomMemberContent>()
                 }
                 liveEventManager.get().dispatchPaginatedEventReceived(event, roomId)
-                currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser)
+                currentChunk.addTimelineEvent(
+                        roomId = roomId,
+                        eventEntity = eventEntity,
+                        direction = direction,
+                        roomMemberContentsByUser = roomMemberContentsByUser)
                 if (lightweightSettingsStorage.areThreadMessagesEnabled()) {
                     eventEntity.rootThreadEventId?.let {
                         // This is a thread event
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 99e6521eb70..1aa01623541 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
@@ -46,10 +46,12 @@ 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.RoomEntity
 import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
+import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
 import org.matrix.android.sdk.internal.database.model.deleteOnCascade
 import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
 import org.matrix.android.sdk.internal.database.query.find
 import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
+import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfThread
 import org.matrix.android.sdk.internal.database.query.getOrCreate
 import org.matrix.android.sdk.internal.database.query.getOrNull
 import org.matrix.android.sdk.internal.database.query.where
@@ -343,6 +345,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
         return roomEntity
     }
 
+    val customList = arrayListOf<String>()
     private fun handleTimelineEvents(realm: Realm,
                                      roomId: String,
                                      roomEntity: RoomEntity,
@@ -406,11 +409,18 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                 rootStateEvent?.asDomain()?.getFixedRoomMemberContent()
             }
 
-            chunkEntity.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser)
+            val timelineEventAdded = chunkEntity.addTimelineEvent(
+                    roomId = roomId,
+                    eventEntity = eventEntity,
+                    direction = PaginationDirection.FORWARDS,
+                    roomMemberContentsByUser = roomMemberContentsByUser)
             if (lightweightSettingsStorage.areThreadMessagesEnabled()) {
                 eventEntity.rootThreadEventId?.let {
                     // This is a thread event
                     optimizedThreadSummaryMap[it] = eventEntity
+                    // Add the same thread timeline event to Thread Chunk
+                    addToThreadChunkIfNeeded(realm, roomId, it, timelineEventAdded, roomEntity)
+
                 } ?: run {
                     // This is a normal event or a root thread one
                     optimizedThreadSummaryMap[eventEntity.eventId] = eventEntity
@@ -455,6 +465,29 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
         return chunkEntity
     }
 
+    /**
+     * Adds new event to the appropriate thread chunk. If the event is already in
+     * the thread timeline and /relations api, we should not added it
+     */
+    private fun addToThreadChunkIfNeeded(realm: Realm,
+                                         roomId: String,
+                                         threadId: String,
+                                         timelineEventEntity: TimelineEventEntity?,
+                                         roomEntity: RoomEntity) {
+
+        val eventId = timelineEventEntity?.eventId ?: return
+
+        ChunkEntity.findLastForwardChunkOfThread(realm, roomId, threadId)?.let { threadChunk ->
+            val existingEvent = threadChunk.timelineEvents.find(eventId)
+            if (existingEvent?.ownedByThreadChunk == true) {
+                Timber.i("###THREADS RoomSyncHandler event:${timelineEventEntity.eventId} already exists, do not add")
+                return@addToThreadChunkIfNeeded
+            }
+            threadChunk.timelineEvents.add(0, timelineEventEntity)
+            roomEntity.addIfNecessary(threadChunk)
+        }
+    }
+
     private fun decryptIfNeeded(event: Event, roomId: String) {
         try {
             // Event from sync does not have roomId, so add it to the event first
diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt
index 8bc6bd73e90..32cb0068102 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt
@@ -65,7 +65,7 @@ class ThreadListController @Inject constructor(
                         id(timelineEvent.eventId)
                         avatarRenderer(host.avatarRenderer)
                         matrixItem(timelineEvent.senderInfo.toMatrixItem())
-                        title(timelineEvent.senderInfo.displayName)
+                        title(timelineEvent.senderInfo.displayName.orEmpty())
                         date(date)
                         rootMessageDeleted(timelineEvent.root.isRedacted())
                         threadNotificationState(timelineEvent.root.threadDetails?.threadNotificationState ?: ThreadNotificationState.NO_NEW_MESSAGE)

From 83d937b842b28acd5f34a1c9120de812dbfaa12b Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Mon, 14 Feb 2022 15:10:30 +0200
Subject: [PATCH 02/38] format ktlint

---
 .../org/matrix/android/sdk/api/session/events/model/Event.kt  | 2 +-
 .../sdk/internal/database/RealmSessionStoreMigration.kt       | 3 +--
 .../matrix/android/sdk/internal/database/model/ChunkEntity.kt | 4 ----
 .../session/room/relation/threads/FetchThreadTimelineTask.kt  | 1 -
 .../internal/session/room/timeline/LoadTimelineStrategy.kt    | 3 +--
 .../sdk/internal/session/room/timeline/TimelineChunk.kt       | 1 -
 .../sdk/internal/session/sync/handler/room/RoomSyncHandler.kt | 2 --
 7 files changed, 3 insertions(+), 13 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 ed9a0573751..97eee9188c5 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
@@ -202,7 +202,7 @@ data class Event(
     fun getDecryptedTextSummary(): String? {
         if (isRedacted()) return "Message Deleted"
         val text = getDecryptedValue() ?: run {
-            if (isPoll()) {return getPollQuestion() ?: "created a poll."}
+            if (isPoll()) { return getPollQuestion() ?: "created a poll." }
             return null
         }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
index 056c4b0ceb3..5706a4ca068 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
@@ -491,7 +491,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
                 ?.setNullable(PreviewUrlCacheEntityFields.IMAGE_HEIGHT, true)
     }
 
-    private fun migrateTo25(realm: DynamicRealm){
+    private fun migrateTo25(realm: DynamicRealm) {
         Timber.d("Step 24 -> 25")
         realm.schema.get("ChunkEntity")
                 ?.addField(ChunkEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED)
@@ -499,6 +499,5 @@ internal class RealmSessionStoreMigration @Inject constructor(
 
         realm.schema.get("TimelineEventEntity")
                 ?.addField(TimelineEventEntityFields.OWNED_BY_THREAD_CHUNK, Boolean::class.java)
-
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt
index 8e4d6fc9163..ca8049fd965 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt
@@ -23,7 +23,6 @@ import io.realm.annotations.Index
 import io.realm.annotations.LinkingObjects
 import org.matrix.android.sdk.internal.extensions.assertIsManaged
 import org.matrix.android.sdk.internal.extensions.clearWith
-import timber.log.Timber
 
 internal open class ChunkEntity(@Index var prevToken: String? = null,
         // Because of gaps we can have several chunks with nextToken == null
@@ -75,6 +74,3 @@ internal fun ChunkEntity.deleteAndClearThreadEvents() {
             }
     deleteFromRealm()
 }
-
-
-
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
index 6b071fbd6e3..e7b91ebab74 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
@@ -118,7 +118,6 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor(
 
     private suspend fun handleRelationsResponse(response: RelationsResponse,
                                                 params: FetchThreadTimelineTask.Params): Result {
-
         val threadList = response.chunks
         val threadRootEvent = response.originalEvent
         val hasReachEnd = response.nextBatch == null
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
index 867589ccc07..a26008369a6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
@@ -179,7 +179,7 @@ internal class LoadTimelineStrategy(
         getContextLatch?.cancel()
         chunkEntity = null
         timelineChunk = null
-        if(mode is Mode.Thread) {
+        if (mode is Mode.Thread) {
             clearThreadChunkEntity(dependencies.realm.get(), mode.rootThreadEventId)
         }
     }
@@ -272,7 +272,6 @@ internal class LoadTimelineStrategy(
             ChunkEntity.findLastForwardChunkOfThread(it, roomId, rootThreadEventId)?.deleteAndClearThreadEvents()?.let {
                 Timber.i("###THREADS LoadTimelineStrategy [onStop] thread chunk cleared..")
             }
-
         }
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
index 7f6e5b6c7c9..864b3f0dd9b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
@@ -171,7 +171,6 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
      * always fetch results, while we want our data to be up to dated.
      */
     suspend fun loadMoreThread(count: Int, direction: Timeline.Direction): LoadMoreResult {
-
         return if (direction == Timeline.Direction.BACKWARDS) {
             try {
                 fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params(
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 1aa01623541..573af7c696f 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
@@ -420,7 +420,6 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                     optimizedThreadSummaryMap[it] = eventEntity
                     // Add the same thread timeline event to Thread Chunk
                     addToThreadChunkIfNeeded(realm, roomId, it, timelineEventAdded, roomEntity)
-
                 } ?: run {
                     // This is a normal event or a root thread one
                     optimizedThreadSummaryMap[eventEntity.eventId] = eventEntity
@@ -474,7 +473,6 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                                          threadId: String,
                                          timelineEventEntity: TimelineEventEntity?,
                                          roomEntity: RoomEntity) {
-
         val eventId = timelineEventEntity?.eventId ?: return
 
         ChunkEntity.findLastForwardChunkOfThread(realm, roomId, threadId)?.let { threadChunk ->

From 27bc43c24c5d3ba56c09e7a05f1e3c2e3e5d9288 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Mon, 14 Feb 2022 15:33:51 +0200
Subject: [PATCH 03/38] Fix realm migration

---
 .../database/RealmSessionStoreMigration.kt    |  4 ++-
 .../database/migration/MigrateSessionTo025.kt | 35 +++++++++++++++++++
 2 files changed, 38 insertions(+), 1 deletion(-)
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo025.kt

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
index f4a8ae2c67c..12e60da1145 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
@@ -42,6 +42,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo021
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo022
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo023
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo024
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo025
 import org.matrix.android.sdk.internal.util.Normalizer
 import timber.log.Timber
 import javax.inject.Inject
@@ -56,7 +57,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
     override fun equals(other: Any?) = other is RealmSessionStoreMigration
     override fun hashCode() = 1000
 
-    val schemaVersion = 24L
+    val schemaVersion = 25L
 
     override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
         Timber.d("Migrating Realm Session from $oldVersion to $newVersion")
@@ -85,5 +86,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
         if (oldVersion < 22) MigrateSessionTo022(realm).perform()
         if (oldVersion < 23) MigrateSessionTo023(realm).perform()
         if (oldVersion < 24) MigrateSessionTo024(realm).perform()
+        if (oldVersion < 25) MigrateSessionTo025(realm).perform()
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo025.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo025.kt
new file mode 100644
index 00000000000..2a859d8b5ad
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo025.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2022 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.database.migration
+
+import io.realm.DynamicRealm
+import io.realm.FieldAttribute
+import org.matrix.android.sdk.internal.database.model.ChunkEntityFields
+import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+class MigrateSessionTo025(realm: DynamicRealm) : RealmMigrator(realm, 24) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.get("ChunkEntity")
+                ?.addField(ChunkEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED)
+                ?.addField(ChunkEntityFields.IS_LAST_FORWARD_THREAD, Boolean::class.java, FieldAttribute.INDEXED)
+
+        realm.schema.get("TimelineEventEntity")
+                ?.addField(TimelineEventEntityFields.OWNED_BY_THREAD_CHUNK, Boolean::class.java)
+    }
+}

From e9e5d680a1a247d552c2a2869f7caa1ca69df3b4 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Mon, 14 Feb 2022 16:51:56 +0200
Subject: [PATCH 04/38] Fix realm migration from 25 to 26

---
 .../database/RealmSessionStoreMigration.kt    |  2 ++
 .../database/migration/MigrateSessionTo026.kt | 36 +++++++++++++++++++
 2 files changed, 38 insertions(+)
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
index 12e60da1145..e84bdc2d309 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
@@ -43,6 +43,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo022
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo023
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo024
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo025
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo026
 import org.matrix.android.sdk.internal.util.Normalizer
 import timber.log.Timber
 import javax.inject.Inject
@@ -87,5 +88,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
         if (oldVersion < 23) MigrateSessionTo023(realm).perform()
         if (oldVersion < 24) MigrateSessionTo024(realm).perform()
         if (oldVersion < 25) MigrateSessionTo025(realm).perform()
+        if (oldVersion < 26) MigrateSessionTo026(realm).perform()
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt
new file mode 100644
index 00000000000..d499365bb3f
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2022 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.database.migration
+
+import io.realm.DynamicRealm
+import io.realm.FieldAttribute
+import org.matrix.android.sdk.internal.database.model.ChunkEntityFields
+import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+class MigrateSessionTo026(realm: DynamicRealm) : RealmMigrator(realm, 26) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+
+        realm.schema.get("ChunkEntity")
+                ?.addField(ChunkEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED)
+                ?.addField(ChunkEntityFields.IS_LAST_FORWARD_THREAD, Boolean::class.java, FieldAttribute.INDEXED)
+
+        realm.schema.get("TimelineEventEntity")
+                ?.addField(TimelineEventEntityFields.OWNED_BY_THREAD_CHUNK, Boolean::class.java)
+    }
+}

From 830c38f50b38a47fc8bae0229b9dd15a164d76e3 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Mon, 14 Feb 2022 16:53:29 +0200
Subject: [PATCH 05/38] format ktlint

---
 .../sdk/internal/database/migration/MigrateSessionTo026.kt       | 1 -
 1 file changed, 1 deletion(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt
index d499365bb3f..597d6d1cbe3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt
@@ -25,7 +25,6 @@ import org.matrix.android.sdk.internal.util.database.RealmMigrator
 class MigrateSessionTo026(realm: DynamicRealm) : RealmMigrator(realm, 26) {
 
     override fun doMigrate(realm: DynamicRealm) {
-
         realm.schema.get("ChunkEntity")
                 ?.addField(ChunkEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED)
                 ?.addField(ChunkEntityFields.IS_LAST_FORWARD_THREAD, Boolean::class.java, FieldAttribute.INDEXED)

From 83088bbe5ac8d6630d2b7657f6d486c432e46b9f Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Fri, 18 Feb 2022 17:21:10 +0200
Subject: [PATCH 06/38] Introduce live thread summaries using the enhanced
 /messages API from MSC 3440 Add capabilities to support local thread list to
 not supported servers

---
 .../org/matrix/android/sdk/flow/FlowRoom.kt   |   8 +-
 .../events/model/AggregatedRelations.kt       |   3 +-
 .../model/LatestThreadUnsignedRelation.kt     |  30 ++
 .../homeserver/HomeServerCapabilities.kt      |   6 +-
 .../android/sdk/api/session/room/Room.kt      |   2 +
 .../room/model/relation/RelationService.kt    |   9 -
 .../session/room/threads/ThreadsService.kt    |  48 ++-
 .../room/threads/local/ThreadsLocalService.kt |  68 ++++
 .../room/threads/model/ThreadEditions.kt      |  20 ++
 .../room/threads/model/ThreadSummary.kt       |  33 ++
 .../threads/model/ThreadSummaryUpdateType.kt  |  22 ++
 .../matrix/android/sdk/api/util/MatrixItem.kt |   3 +
 .../database/RealmSessionStoreMigration.kt    |   4 +-
 .../database/helper/ChunkEntityHelper.kt      |   2 +-
 .../database/helper/RoomEntityHelper.kt       |   7 +
 .../database/helper/ThreadEventsHelper.kt     |   6 +-
 .../database/helper/ThreadSummaryHelper.kt    | 332 ++++++++++++++++++
 .../mapper/HomeServerCapabilitiesMapper.kt    |   3 +-
 .../database/mapper/ThreadSummaryMapper.kt    |  48 +++
 .../database/migration/MigrateSessionTo027.kt |  49 +++
 .../internal/database/model/ChunkEntity.kt    |   7 +-
 .../internal/database/model/EventEntity.kt    |   4 +-
 .../model/HomeServerCapabilitiesEntity.kt     |   3 +-
 .../sdk/internal/database/model/RoomEntity.kt |  11 +
 .../database/model/SessionRealmModule.kt      |   4 +-
 .../model/threads/ThreadSummaryEntity.kt      |  43 +++
 .../query/ThreadSummaryEntityQueries.kt       |  59 ++++
 .../internal/session/filter/FilterFactory.kt  |  16 +-
 .../session/filter/RoomEventFilter.kt         |   7 +-
 .../homeserver/GetCapabilitiesResult.kt       |   8 +-
 .../GetHomeServerCapabilitiesTask.kt          |   1 +
 .../sdk/internal/session/room/DefaultRoom.kt  |   3 +
 .../EventRelationsAggregationProcessor.kt     |  14 +-
 .../sdk/internal/session/room/RoomAPI.kt      |   2 +-
 .../sdk/internal/session/room/RoomFactory.kt  |   3 +
 .../sdk/internal/session/room/RoomModule.kt   |   5 +
 .../room/relation/DefaultRelationService.kt   |  12 -
 .../threads/FetchThreadSummariesTask.kt       | 108 ++++++
 .../threads/FetchThreadTimelineTask.kt        |   1 -
 .../room/threads/DefaultThreadsService.kt     |  75 ++--
 .../local/DefaultThreadsLocalService.kt       | 103 ++++++
 .../sync/handler/room/RoomSyncHandler.kt      |  18 +-
 .../home/room/threads/ThreadsActivity.kt      |  14 +-
 .../list/viewmodel/ThreadListController.kt    |  62 +++-
 .../list/viewmodel/ThreadListViewModel.kt     |  41 ++-
 .../list/viewmodel/ThreadListViewState.kt     |   3 +-
 .../threads/list/views/ThreadListFragment.kt  |  28 +-
 47 files changed, 1220 insertions(+), 138 deletions(-)
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/LatestThreadUnsignedRelation.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/local/ThreadsLocalService.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadEditions.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummary.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummaryUpdateType.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ThreadSummaryMapper.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo027.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ThreadSummaryEntityQueries.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/local/DefaultThreadsLocalService.kt

diff --git a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt
index 826f584f6a9..fb8bf2df274 100644
--- a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt
+++ b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt
@@ -28,6 +28,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
 import org.matrix.android.sdk.api.session.room.model.RoomSummary
 import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState
 import org.matrix.android.sdk.api.session.room.send.UserDraft
+import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
 import org.matrix.android.sdk.api.util.Optional
 import org.matrix.android.sdk.api.util.toOptional
@@ -101,13 +102,18 @@ class FlowRoom(private val room: Room) {
         return room.getLiveRoomNotificationState().asFlow()
     }
 
+    fun liveThreadSummaries(): Flow<List<ThreadSummary>> {
+        return room.getAllThreadSummariesLive().asFlow()
+                .startWith(room.coroutineDispatchers.io) {
+                    room.getAllThreadSummaries()
+                }
+    }
     fun liveThreadList(): Flow<List<ThreadRootEvent>> {
         return room.getAllThreadsLive().asFlow()
                 .startWith(room.coroutineDispatchers.io) {
                     room.getAllThreads()
                 }
     }
-
     fun liveLocalUnreadThreadList(): Flow<List<ThreadRootEvent>> {
         return room.getMarkedThreadNotificationsLive().asFlow()
                 .startWith(room.coroutineDispatchers.io) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt
index 34096d603fd..7547d1cfe97 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt
@@ -49,5 +49,6 @@ import com.squareup.moshi.JsonClass
 @JsonClass(generateAdapter = true)
 data class AggregatedRelations(
         @Json(name = "m.annotation") val annotations: AggregatedAnnotation? = null,
-        @Json(name = "m.reference") val references: DefaultUnsignedRelationInfo? = null
+        @Json(name = "m.reference") val references: DefaultUnsignedRelationInfo? = null,
+        @Json(name = RelationType.IO_THREAD) val latestThread: LatestThreadUnsignedRelation? = null
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/LatestThreadUnsignedRelation.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/LatestThreadUnsignedRelation.kt
new file mode 100644
index 00000000000..cc52dfc02c1
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/LatestThreadUnsignedRelation.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2022 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.api.session.events.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class LatestThreadUnsignedRelation(
+        override val limited: Boolean? = false,
+        override val count: Int? = 0,
+        @Json(name = "latest_event")
+        val event: Event? = null,
+        @Json(name = "current_user_participated")
+        val isUserParticipating: Boolean? = false
+
+) : UnsignedRelationInfo
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt
index 2256dfb8f0c..9db3876b747 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt
@@ -50,7 +50,11 @@ data class HomeServerCapabilities(
          * This capability describes the default and available room versions a server supports, and at what level of stability.
          * Clients should make use of this capability to determine if users need to be encouraged to upgrade their rooms.
          */
-        val roomVersions: RoomVersionCapabilities? = null
+        val roomVersions: RoomVersionCapabilities? = null,
+        /**
+         * True if the home server support threading
+         */
+        var canUseThreading: Boolean = false
 ) {
 
     enum class RoomCapabilitySupport {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt
index d930a5d0fd6..be65b883b36 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt
@@ -33,6 +33,7 @@ import org.matrix.android.sdk.api.session.room.send.SendService
 import org.matrix.android.sdk.api.session.room.state.StateService
 import org.matrix.android.sdk.api.session.room.tags.TagsService
 import org.matrix.android.sdk.api.session.room.threads.ThreadsService
+import org.matrix.android.sdk.api.session.room.threads.local.ThreadsLocalService
 import org.matrix.android.sdk.api.session.room.timeline.TimelineService
 import org.matrix.android.sdk.api.session.room.typing.TypingService
 import org.matrix.android.sdk.api.session.room.uploads.UploadsService
@@ -47,6 +48,7 @@ import org.matrix.android.sdk.api.util.Optional
 interface Room :
         TimelineService,
         ThreadsService,
+        ThreadsLocalService,
         SendService,
         DraftService,
         ReadService,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt
index 09114436f04..44098989084 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt
@@ -163,13 +163,4 @@ interface RelationService {
                       autoMarkdown: Boolean = false,
                       formattedText: String? = null,
                       eventReplied: TimelineEvent? = null): Cancelable?
-
-    /**
-     * Get all the thread replies for the specified rootThreadEventId
-     * The return list will contain the original root thread event and all the thread replies to that event
-     * Note: We will use a large limit value in order to avoid using pagination until it would be 100% ready
-     * from the backend
-     * @param rootThreadEventId the root thread eventId
-     */
-    suspend fun fetchThreadTimeline(rootThreadEventId: String): Boolean
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt
index e4d1d979e1a..99c0dc7d0fa 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt
@@ -17,51 +17,43 @@
 package org.matrix.android.sdk.api.session.room.threads
 
 import androidx.lifecycle.LiveData
-import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
 
 /**
- * This interface defines methods to interact with threads related features.
- * It's implemented at the room level within the main timeline.
+ * This interface defines methods to interact with thread related features.
+ * It's the dynamic threads implementation and the homeserver must return
+ * a capability entry for threads. If the server do not support m.thread
+ * then [ThreadsLocalService] should be used instead
  */
 interface ThreadsService {
 
     /**
-     * Returns a [LiveData] list of all the thread root TimelineEvents that exists at the room level
+     * Returns a [LiveData] list of all the [ThreadSummary] that exists at the room level
      */
-    fun getAllThreadsLive(): LiveData<List<TimelineEvent>>
+    fun getAllThreadSummariesLive(): LiveData<List<ThreadSummary>>
 
     /**
-     * Returns a list of all the thread root TimelineEvents that exists at the room level
+     * Returns a list of all the [ThreadSummary] that exists at the room level
      */
-    fun getAllThreads(): List<TimelineEvent>
+    fun getAllThreadSummaries(): List<ThreadSummary>
 
     /**
-     * Returns a [LiveData] list of all the marked unread threads that exists at the room level
-     */
-    fun getMarkedThreadNotificationsLive(): LiveData<List<TimelineEvent>>
-
-    /**
-     * Returns a list of all the marked unread threads that exists at the room level
-     */
-    fun getMarkedThreadNotifications(): List<TimelineEvent>
-
-    /**
-     * Returns whether or not the current user is participating in the thread
-     * @param rootThreadEventId the eventId of the current thread
+     * Enhance the provided ThreadSummary[List] by adding the latest
+     * message edition for that thread
+     * @return the enhanced [List] with edited updates
      */
-    fun isUserParticipatingInThread(rootThreadEventId: String): Boolean
+    fun enhanceWithEditions(threads: List<ThreadSummary>): List<ThreadSummary>
 
     /**
-     * Enhance the provided root thread TimelineEvent [List] by adding the latest
-     * message edition for that thread
-     * @return the enhanced [List] with edited updates
+     * Fetch all thread replies for the specified thread using the /relations api
+     * @param rootThreadEventId the root thread eventId
+     * @param from defines the token that will fetch from that position
+     * @param limit defines the number of max results the api will respond with
      */
-    fun mapEventsWithEdition(threads: List<TimelineEvent>): List<TimelineEvent>
+    suspend fun fetchThreadTimeline(rootThreadEventId: String, from: String, limit: Int)
 
     /**
-     * Marks the current thread as read in local DB.
-     * note: read receipts within threads are not yet supported with the API
-     * @param rootThreadEventId the root eventId of the current thread
+     * Fetch all thread summaries for the current room using the enhanced /messages api
      */
-    suspend fun markThreadAsRead(rootThreadEventId: String)
+    suspend fun fetchThreadSummaries()
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/local/ThreadsLocalService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/local/ThreadsLocalService.kt
new file mode 100644
index 00000000000..f7b379e3821
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/local/ThreadsLocalService.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2022 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.api.session.room.threads.local
+
+import androidx.lifecycle.LiveData
+import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+
+/**
+ * This interface defines methods to interact with thread related features.
+ * It's the local threads implementation and assumes that the homeserver
+ * do not support threads
+ */
+interface ThreadsLocalService {
+
+    /**
+     * Returns a [LiveData] list of all the thread root TimelineEvents that exists at the room level
+     */
+    fun getAllThreadsLive(): LiveData<List<TimelineEvent>>
+
+    /**
+     * Returns a list of all the thread root TimelineEvents that exists at the room level
+     */
+    fun getAllThreads(): List<TimelineEvent>
+
+    /**
+     * Returns a [LiveData] list of all the marked unread threads that exists at the room level
+     */
+    fun getMarkedThreadNotificationsLive(): LiveData<List<TimelineEvent>>
+
+    /**
+     * Returns a list of all the marked unread threads that exists at the room level
+     */
+    fun getMarkedThreadNotifications(): List<TimelineEvent>
+
+    /**
+     * Returns whether or not the current user is participating in the thread
+     * @param rootThreadEventId the eventId of the current thread
+     */
+    fun isUserParticipatingInThread(rootThreadEventId: String): Boolean
+
+    /**
+     * Enhance the provided root thread TimelineEvent [List] by adding the latest
+     * message edition for that thread
+     * @return the enhanced [List] with edited updates
+     */
+    fun mapEventsWithEdition(threads: List<TimelineEvent>): List<TimelineEvent>
+
+    /**
+     * Marks the current thread as read in local DB.
+     * note: read receipts within threads are not yet supported with the API
+     * @param rootThreadEventId the root eventId of the current thread
+     */
+    suspend fun markThreadAsRead(rootThreadEventId: String)
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadEditions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadEditions.kt
new file mode 100644
index 00000000000..db92e800e48
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadEditions.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * 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.api.session.room.threads.model
+
+data class ThreadEditions(var rootThreadEdition: String? = null,
+                          var latestThreadEdition: String? = null)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummary.kt
new file mode 100644
index 00000000000..f26be85e854
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummary.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * 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.api.session.room.threads.model
+
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.room.sender.SenderInfo
+
+/**
+ * The main thread Summary model, mainly used to display the thread list
+ */
+data class ThreadSummary(val roomId: String,
+                         val rootEvent: Event?,
+                         val latestEvent: Event?,
+                         val rootEventId: String,
+                         val rootThreadSenderInfo: SenderInfo,
+                         val latestThreadSenderInfo: SenderInfo,
+                         val isUserParticipating: Boolean,
+                         val numberOfThreads: Int,
+                         val threadEditions: ThreadEditions = ThreadEditions())
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummaryUpdateType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummaryUpdateType.kt
new file mode 100644
index 00000000000..744265cb944
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummaryUpdateType.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * 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.api.session.room.threads.model
+
+enum class ThreadSummaryUpdateType {
+    REPLACE,
+    ADD
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt
index 3396c4a6c98..17d7d96a38a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt
@@ -17,6 +17,7 @@
 package org.matrix.android.sdk.api.util
 
 import org.matrix.android.sdk.BuildConfig
+import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.session.group.model.GroupSummary
 import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
 import org.matrix.android.sdk.api.session.room.model.RoomSummary
@@ -178,6 +179,8 @@ fun RoomMemberSummary.toMatrixItem() = MatrixItem.UserItem(userId, displayName,
 
 fun SenderInfo.toMatrixItem() = MatrixItem.UserItem(userId, disambiguatedDisplayName, avatarUrl)
 
+fun SenderInfo.toMatrixItemOrNull() = tryOrNull {  MatrixItem.UserItem(userId, disambiguatedDisplayName, avatarUrl) }
+
 fun SpaceChildInfo.toMatrixItem() = if (roomType == RoomType.SPACE) {
     MatrixItem.SpaceItem(childRoomId, name ?: canonicalAlias, avatarUrl)
 } else {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
index e84bdc2d309..24ac3106532 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
@@ -44,6 +44,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo023
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo024
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo025
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo026
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo027
 import org.matrix.android.sdk.internal.util.Normalizer
 import timber.log.Timber
 import javax.inject.Inject
@@ -58,7 +59,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
     override fun equals(other: Any?) = other is RealmSessionStoreMigration
     override fun hashCode() = 1000
 
-    val schemaVersion = 25L
+    val schemaVersion = 27L
 
     override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
         Timber.d("Migrating Realm Session from $oldVersion to $newVersion")
@@ -89,5 +90,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
         if (oldVersion < 24) MigrateSessionTo024(realm).perform()
         if (oldVersion < 25) MigrateSessionTo025(realm).perform()
         if (oldVersion < 26) MigrateSessionTo026(realm).perform()
+        if (oldVersion < 27) MigrateSessionTo027(realm).perform()
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt
index 007017510c3..d2e3e99b755 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt
@@ -118,7 +118,7 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String,
     return timelineEventEntity
 }
 
-private fun computeIsUnique(
+fun computeIsUnique(
         realm: Realm,
         roomId: String,
         isLastForward: Boolean,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/RoomEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/RoomEntityHelper.kt
index 724f307e3bd..9ad2708b438 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/RoomEntityHelper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/RoomEntityHelper.kt
@@ -18,9 +18,16 @@ package org.matrix.android.sdk.internal.database.helper
 
 import org.matrix.android.sdk.internal.database.model.ChunkEntity
 import org.matrix.android.sdk.internal.database.model.RoomEntity
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
 
 internal fun RoomEntity.addIfNecessary(chunkEntity: ChunkEntity) {
     if (!chunks.contains(chunkEntity)) {
         chunks.add(chunkEntity)
     }
 }
+
+internal fun RoomEntity.addIfNecessary(threadSummary: ThreadSummaryEntity) {
+    if (!threadSummaries.contains(threadSummary)) {
+        threadSummaries.add(threadSummary)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
index 7f6b64da75d..ee3008d40b1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
@@ -34,7 +34,7 @@ import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
 import org.matrix.android.sdk.internal.database.query.where
 import org.matrix.android.sdk.internal.database.query.whereRoomId
 
-private typealias ThreadSummary = Pair<Int, TimelineEventEntity>?
+private typealias Summary = Pair<Int, TimelineEventEntity>?
 
 /**
  * Finds the root thread event and update it with the latest message summary along with the number
@@ -93,7 +93,7 @@ internal fun EventEntity.markEventAsRoot(
  * @param rootThreadEventId The root eventId that will find the number of threads
  * @return A ThreadSummary containing the counted threads and the latest event message
  */
-internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId: String, chunkEntity: ChunkEntity?): ThreadSummary {
+internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId: String, chunkEntity: ChunkEntity?): Summary {
     // Number of messages
     val messages = TimelineEventEntity
             .whereRoomId(realm, roomId = roomId)
@@ -124,7 +124,7 @@ internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId:
 
     result ?: return null
 
-    return ThreadSummary(messages, result)
+    return Summary(messages, result)
 }
 
 /**
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
new file mode 100644
index 00000000000..d19056adfa8
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt
@@ -0,0 +1,332 @@
+/*
+ * Copyright 2022 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.database.helper
+
+import io.realm.Realm
+import io.realm.RealmQuery
+import io.realm.Sort
+import io.realm.kotlin.createObject
+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.EventType
+import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
+import org.matrix.android.sdk.api.session.room.send.SendState
+import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
+import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummaryUpdateType
+import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
+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.CurrentStateEventEntity
+import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
+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.RoomEntity
+import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields
+import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
+import org.matrix.android.sdk.internal.database.query.getOrCreate
+import org.matrix.android.sdk.internal.database.query.getOrNull
+import org.matrix.android.sdk.internal.database.query.where
+import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent
+import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor
+import timber.log.Timber
+import java.util.UUID
+
+internal fun ThreadSummaryEntity.updateThreadSummary(
+        rootThreadEventEntity: EventEntity,
+        numberOfThreads: Int?,
+        latestThreadEventEntity: EventEntity?,
+        isUserParticipating: Boolean,
+        roomMemberContentsByUser: HashMap<String, RoomMemberContent?>) {
+    updateThreadSummaryRootEvent(rootThreadEventEntity, roomMemberContentsByUser)
+    updateThreadSummaryLatestEvent(latestThreadEventEntity, roomMemberContentsByUser)
+
+    // Update latest event
+//    latestThreadEventEntity?.toTimelineEventEntity(roomMemberContentsByUser)?.let {
+//        Timber.i("###THREADS FetchThreadSummariesTask ThreadSummaryEntity updated latest event:${it.eventId} !")
+//        this.eventEntity?.threadSummaryLatestMessage = it
+//    }
+
+    // Update number of threads
+    this.isUserParticipating = isUserParticipating
+    numberOfThreads?.let {
+        // Update only when there is an actual value
+        this.numberOfThreads = it
+    }
+}
+
+/**
+ * Updates the root thread event properties
+ */
+internal fun ThreadSummaryEntity.updateThreadSummaryRootEvent(
+        rootThreadEventEntity: EventEntity,
+        roomMemberContentsByUser: HashMap<String, RoomMemberContent?>
+) {
+    val roomId = rootThreadEventEntity.roomId
+    val rootThreadRoomMemberContent = roomMemberContentsByUser[rootThreadEventEntity.sender ?: ""]
+    this.rootThreadEventEntity = rootThreadEventEntity
+    this.rootThreadSenderAvatar = rootThreadRoomMemberContent?.avatarUrl
+    this.rootThreadSenderName = rootThreadRoomMemberContent?.displayName
+    this.rootThreadIsUniqueDisplayName = if (rootThreadRoomMemberContent?.displayName != null) {
+        computeIsUnique(realm, roomId, false, rootThreadRoomMemberContent, roomMemberContentsByUser)
+    } else {
+        true
+    }
+}
+
+/**
+ * Updates the latest thread event properties
+ */
+internal fun ThreadSummaryEntity.updateThreadSummaryLatestEvent(
+        latestThreadEventEntity: EventEntity?,
+        roomMemberContentsByUser: HashMap<String, RoomMemberContent?>
+) {
+    val roomId = latestThreadEventEntity?.roomId ?: return
+    val latestThreadRoomMemberContent = roomMemberContentsByUser[latestThreadEventEntity.sender ?: ""]
+    this.latestThreadEventEntity = latestThreadEventEntity
+    this.latestThreadSenderAvatar = latestThreadRoomMemberContent?.avatarUrl
+    this.latestThreadSenderName = latestThreadRoomMemberContent?.displayName
+    this.latestThreadIsUniqueDisplayName = if (latestThreadRoomMemberContent?.displayName != null) {
+        computeIsUnique(realm, roomId, false, latestThreadRoomMemberContent, roomMemberContentsByUser)
+    } else {
+        true
+    }
+}
+
+private fun EventEntity.toTimelineEventEntity(roomMemberContentsByUser: HashMap<String, RoomMemberContent?>): TimelineEventEntity {
+    val roomId = roomId
+    val eventId = eventId
+    val localId = TimelineEventEntity.nextId(realm)
+    val senderId = sender ?: ""
+
+    val timelineEventEntity = realm.createObject<TimelineEventEntity>().apply {
+        this.localId = localId
+        this.root = this@toTimelineEventEntity
+        this.eventId = eventId
+        this.roomId = roomId
+        this.annotations = EventAnnotationsSummaryEntity.where(realm, roomId, eventId).findFirst()
+                ?.also { it.cleanUp(sender) }
+        this.ownedByThreadChunk = true  // To skip it from the original event flow
+        val roomMemberContent = roomMemberContentsByUser[senderId]
+        this.senderAvatar = roomMemberContent?.avatarUrl
+        this.senderName = roomMemberContent?.displayName
+        isUniqueDisplayName = if (roomMemberContent?.displayName != null) {
+            computeIsUnique(realm, roomId, false, roomMemberContent, roomMemberContentsByUser)
+        } else {
+            true
+        }
+    }
+    return timelineEventEntity
+}
+
+internal fun ThreadSummaryEntity.Companion.createOrUpdate(
+        threadSummaryType: ThreadSummaryUpdateType,
+        realm: Realm,
+        roomId: String,
+        threadEventEntity: EventEntity? = null,
+        rootThreadEvent: Event? = null,
+        roomMemberContentsByUser: HashMap<String, RoomMemberContent?>,
+        roomEntity: RoomEntity,
+        userId: String,
+        cryptoService: CryptoService? = null
+) {
+    when (threadSummaryType) {
+        ThreadSummaryUpdateType.REPLACE -> {
+            rootThreadEvent?.eventId ?: return
+            rootThreadEvent.senderId ?: return
+
+            val numberOfThreads = rootThreadEvent.unsignedData?.relations?.latestThread?.count ?: return
+
+            // Something is wrong with the server return
+            if (numberOfThreads <= 0) return
+
+            val threadSummary = ThreadSummaryEntity.getOrCreate(realm, roomId, rootThreadEvent.eventId).also {
+                Timber.i("###THREADS ThreadSummaryHelper REPLACE eventId:${it.rootThreadEventId} ")
+            }
+
+            val rootThreadEventEntity = createEventEntity(roomId, rootThreadEvent, realm).also {
+                decryptIfNeeded(cryptoService, it, roomId)
+            }
+            val latestThreadEventEntity = createLatestEventEntity(roomId, rootThreadEvent, roomMemberContentsByUser, realm)?.also {
+                decryptIfNeeded(cryptoService, it, roomId)
+            }
+            val isUserParticipating = rootThreadEvent.unsignedData.relations.latestThread.isUserParticipating == true || rootThreadEvent.senderId == userId
+            roomMemberContentsByUser.addSenderState(realm, roomId, rootThreadEvent.senderId)
+            threadSummary.updateThreadSummary(
+                    rootThreadEventEntity = rootThreadEventEntity,
+                    numberOfThreads = numberOfThreads,
+                    latestThreadEventEntity = latestThreadEventEntity,
+                    isUserParticipating = isUserParticipating,
+                    roomMemberContentsByUser = roomMemberContentsByUser
+            )
+
+            roomEntity.addIfNecessary(threadSummary)
+        }
+        ThreadSummaryUpdateType.ADD     -> {
+            val rootThreadEventId = threadEventEntity?.rootThreadEventId ?: return
+            Timber.i("###THREADS ThreadSummaryHelper ADD for root eventId:$rootThreadEventId")
+
+            val threadSummary = ThreadSummaryEntity.getOrNull(realm, roomId, rootThreadEventId)
+            if (threadSummary != null) {
+                // ThreadSummary exists so lets add the latest event
+                Timber.i("###THREADS ThreadSummaryHelper ADD root eventId:$rootThreadEventId exists, lets update latest thread event.")
+                threadSummary.updateThreadSummaryLatestEvent(threadEventEntity, roomMemberContentsByUser)
+                threadSummary.numberOfThreads++
+                if (threadEventEntity.sender == userId) {
+                    threadSummary.isUserParticipating = true
+                }
+            } else {
+                // ThreadSummary do not exists lets try to create one
+                Timber.i("###THREADS ThreadSummaryHelper ADD root eventId:$rootThreadEventId do not exists, lets try to create one")
+                threadEventEntity.findRootThreadEvent()?.let { rootThreadEventEntity ->
+                    // Root thread event entity exists so lets create a new record
+                    ThreadSummaryEntity.getOrCreate(realm, roomId, rootThreadEventEntity.eventId).let {
+                        it.updateThreadSummary(
+                                rootThreadEventEntity = rootThreadEventEntity,
+                                numberOfThreads = 1,
+                                latestThreadEventEntity = threadEventEntity,
+                                isUserParticipating = threadEventEntity.sender == userId,
+                                roomMemberContentsByUser = roomMemberContentsByUser
+                        )
+                        roomEntity.addIfNecessary(it)
+                    }
+                }
+            }
+        }
+    }
+}
+
+private fun decryptIfNeeded(cryptoService: CryptoService?, eventEntity: EventEntity, roomId: String) {
+    cryptoService ?: return
+    val event = eventEntity.asDomain()
+    if (event.isEncrypted() && event.mxDecryptionResult == null && event.eventId != null) {
+        try {
+            Timber.i("###THREADS ThreadSummaryHelper request decryption for eventId:${event.eventId}")
+            // 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
+            )
+            // Save decryption result, to not decrypt every time we enter the thread list
+            eventEntity.setDecryptionResult(result)
+        } catch (e: MXCryptoError) {
+            if (e is MXCryptoError.Base) {
+                event.mCryptoError = e.errorType
+                event.mCryptoErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription
+            }
+        }
+    }
+}
+
+/**
+ * Request decryption
+ */
+private fun requestDecryption(eventDecryptor: TimelineEventDecryptor?, event: Event?) {
+    eventDecryptor ?: return
+    event ?: return
+    if (event.isEncrypted() &&
+            event.mxDecryptionResult == null && event.eventId != null) {
+        Timber.i("###THREADS ThreadSummaryHelper request decryption for eventId:${event.eventId}")
+
+        eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(event, UUID.randomUUID().toString()))
+    }
+}
+
+/**
+ * If we don't have any new state on this user, get it from db
+ */
+private fun HashMap<String, RoomMemberContent?>.addSenderState(realm: Realm, roomId: String, senderId: String) {
+    getOrPut(senderId) {
+        CurrentStateEventEntity
+                .getOrNull(realm, roomId, senderId, EventType.STATE_ROOM_MEMBER)
+                ?.root?.asDomain()
+                ?.getFixedRoomMemberContent()
+    }
+}
+
+/**
+ * Create an EventEntity for the root thread event or get an existing one
+ */
+private fun createEventEntity(roomId: String, event: Event, realm: Realm): EventEntity {
+    val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
+    return event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC)
+}
+
+/**
+ * Create an EventEntity for the latest thread event or get an existing one. Also update the user room member
+ * state
+ */
+private fun createLatestEventEntity(roomId: String, rootThreadEvent: Event, roomMemberContentsByUser: HashMap<String, RoomMemberContent?>, realm: Realm): EventEntity? {
+    return getLatestEvent(rootThreadEvent)?.let {
+        it.senderId?.let { senderId ->
+            roomMemberContentsByUser.addSenderState(realm, roomId, senderId)
+        }
+        createEventEntity(roomId, it, realm)
+    }
+}
+
+/**
+ * Returned the latest event message, if any
+ */
+private fun getLatestEvent(rootThreadEvent: Event): Event? {
+    return rootThreadEvent.unsignedData?.relations?.latestThread?.event
+}
+
+/**
+ * Find all ThreadSummaryEntity for the specified roomId, sorted by origin server
+ * note: Sorting cannot be provided by server, so we have to use that unstable property
+ * @param roomId The id of the room
+ */
+internal fun ThreadSummaryEntity.Companion.findAllThreadsForRoomId(realm: Realm, roomId: String): RealmQuery<ThreadSummaryEntity> =
+        ThreadSummaryEntity
+                .where(realm, roomId = roomId)
+                .sort(ThreadSummaryEntityFields.LATEST_THREAD_EVENT_ENTITY.ORIGIN_SERVER_TS, Sort.DESCENDING)
+
+/**
+ * Enhance each [ThreadSummary] root and latest event with the equivalent decrypted text edition/replacement
+ */
+internal fun List<ThreadSummary>.enhanceWithEditions(realm: Realm, roomId: String): List<ThreadSummary> =
+        this.map {
+            it.addEditionIfNeeded(realm, roomId, true)
+            it.addEditionIfNeeded(realm, roomId, false)
+            it
+        }
+
+private fun ThreadSummary.addEditionIfNeeded(realm: Realm, roomId: String, enhanceRoot: Boolean) {
+    val eventId = if (enhanceRoot) rootEventId else latestEvent?.eventId ?: return
+    EventAnnotationsSummaryEntity
+            .where(realm, roomId, eventId)
+            .findFirst()
+            ?.editSummary
+            ?.editions
+            ?.lastOrNull()
+            ?.eventId
+            ?.let { editedEventId ->
+                TimelineEventEntity.where(realm, roomId, eventId = editedEventId).findFirst()?.let { editedEvent ->
+                    if (enhanceRoot) {
+                        threadEditions.rootThreadEdition = editedEvent.root?.asDomain()?.getDecryptedTextSummary() ?: "(edited)"
+                    } else {
+                        threadEditions.latestThreadEdition = editedEvent.root?.asDomain()?.getDecryptedTextSummary() ?: "(edited)"
+                    }
+                }
+            }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt
index 7869506015e..8be3455c079 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt
@@ -41,7 +41,8 @@ internal object HomeServerCapabilitiesMapper {
                 maxUploadFileSize = entity.maxUploadFileSize,
                 lastVersionIdentityServerSupported = entity.lastVersionIdentityServerSupported,
                 defaultIdentityServerUrl = entity.defaultIdentityServerUrl,
-                roomVersions = mapRoomVersion(entity.roomVersionsJson)
+                roomVersions = mapRoomVersion(entity.roomVersionsJson),
+                canUseThreading = false // entity.canUseThreading
         )
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ThreadSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ThreadSummaryMapper.kt
new file mode 100644
index 00000000000..54386c5ea8c
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ThreadSummaryMapper.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2022 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.database.mapper
+
+import org.matrix.android.sdk.api.session.room.sender.SenderInfo
+import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
+import javax.inject.Inject
+
+internal class ThreadSummaryMapper @Inject constructor() {
+
+    fun map(threadSummary: ThreadSummaryEntity): ThreadSummary {
+        return ThreadSummary(
+                roomId = threadSummary.room?.firstOrNull()?.roomId.orEmpty(),
+                rootEvent = threadSummary.rootThreadEventEntity?.asDomain(),
+                latestEvent = threadSummary.latestThreadEventEntity?.asDomain(),
+                rootEventId = threadSummary.rootThreadEventId,
+                rootThreadSenderInfo = SenderInfo(
+                        userId = threadSummary.rootThreadEventEntity?.sender ?: "",
+                        displayName = threadSummary.rootThreadSenderName,
+                        isUniqueDisplayName = threadSummary.rootThreadIsUniqueDisplayName,
+                        avatarUrl = threadSummary.rootThreadSenderAvatar
+                ),
+                latestThreadSenderInfo = SenderInfo(
+                        userId = threadSummary.latestThreadEventEntity?.sender ?: "",
+                        displayName = threadSummary.latestThreadSenderName,
+                        isUniqueDisplayName = threadSummary.latestThreadIsUniqueDisplayName,
+                        avatarUrl = threadSummary.latestThreadSenderAvatar
+                ),
+                isUserParticipating = threadSummary.isUserParticipating,
+                numberOfThreads = threadSummary.numberOfThreads
+        )
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo027.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo027.kt
new file mode 100644
index 00000000000..b56b7d325b4
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo027.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2022 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.database.migration
+
+import io.realm.DynamicRealm
+import io.realm.FieldAttribute
+import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
+import org.matrix.android.sdk.internal.database.model.RoomEntityFields
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+class MigrateSessionTo027(realm: DynamicRealm) : RealmMigrator(realm, 27) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        val eventEntity = realm.schema.get("EventEntity") ?: return
+        val threadSummaryEntity = realm.schema.create("ThreadSummaryEntity")
+                .addField(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED)
+                .addField(ThreadSummaryEntityFields.ROOT_THREAD_SENDER_NAME, String::class.java)
+                .addField(ThreadSummaryEntityFields.ROOT_THREAD_SENDER_AVATAR, String::class.java)
+                .addField(ThreadSummaryEntityFields.ROOT_THREAD_IS_UNIQUE_DISPLAY_NAME, String::class.java)
+                .addField(ThreadSummaryEntityFields.LATEST_THREAD_SENDER_NAME, String::class.java)
+                .addField(ThreadSummaryEntityFields.LATEST_THREAD_SENDER_AVATAR, String::class.java)
+                .addField(ThreadSummaryEntityFields.LATEST_THREAD_IS_UNIQUE_DISPLAY_NAME, String::class.java)
+                .addField(ThreadSummaryEntityFields.NUMBER_OF_THREADS, Int::class.java)
+                .addField(ThreadSummaryEntityFields.IS_USER_PARTICIPATING, Boolean::class.java)
+                .addRealmObjectField(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ENTITY.`$`, eventEntity)
+                .addRealmObjectField(ThreadSummaryEntityFields.LATEST_THREAD_EVENT_ENTITY.`$`, eventEntity)
+
+        realm.schema.get("RoomEntity")
+                ?.addRealmListField(RoomEntityFields.THREAD_SUMMARIES.`$`, threadSummaryEntity)
+
+        realm.schema.get("HomeServerCapabilitiesEntity")
+                ?.addRealmListField(HomeServerCapabilitiesEntityFields.CAN_USE_THREADING, Boolean::class.java)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt
index ca8049fd965..88eb821aa9d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt
@@ -50,13 +50,18 @@ internal open class ChunkEntity(@Index var prevToken: String? = null,
     companion object
 }
 
-internal fun ChunkEntity.deleteOnCascade(deleteStateEvents: Boolean, canDeleteRoot: Boolean) {
+internal fun ChunkEntity.deleteOnCascade(
+        deleteStateEvents: Boolean,
+        canDeleteRoot: Boolean) {
     assertIsManaged()
     if (deleteStateEvents) {
         stateEvents.deleteAllFromRealm()
     }
     timelineEvents.clearWith {
         val deleteRoot = canDeleteRoot && (it.root?.stateKey == null || deleteStateEvents)
+        if (deleteRoot) {
+            room?.firstOrNull()?.removeThreadSummaryIfNeeded(it.eventId)
+        }
         it.deleteOnCascade(deleteRoot)
     }
     deleteFromRealm()
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 445181e5764..b7158ba9cd8 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
@@ -34,14 +34,14 @@ internal open class EventEntity(@Index var eventId: String = "",
                                 @Index var stateKey: String? = null,
                                 var originServerTs: Long? = null,
                                 @Index var sender: String? = null,
-                                // Can contain a serialized MatrixError
+        // Can contain a serialized MatrixError
                                 var sendStateDetails: String? = null,
                                 var age: Long? = 0,
                                 var unsignedData: String? = null,
                                 var redacts: String? = null,
                                 var decryptionResultJson: String? = null,
                                 var ageLocalTs: Long? = null,
-                                // Thread related, no need to create a new Entity for performance
+        // Thread related, no need to create a new Entity for performance
                                 @Index var isRootThread: Boolean = false,
                                 @Index var rootThreadEventId: String? = null,
                                 var numberOfThreads: Int = 0,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt
index 08ecd5995ec..47a83f0ed99 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt
@@ -28,7 +28,8 @@ internal open class HomeServerCapabilitiesEntity(
         var maxUploadFileSize: Long = HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN,
         var lastVersionIdentityServerSupported: Boolean = false,
         var defaultIdentityServerUrl: String? = null,
-        var lastUpdatedTimestamp: Long = 0L
+        var lastUpdatedTimestamp: Long = 0L,
+        var canUseThreading: Boolean = false
 ) : RealmObject() {
 
     companion object
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt
index 2997d5d7d8a..4a6f6a7bf89 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt
@@ -20,10 +20,14 @@ import io.realm.RealmList
 import io.realm.RealmObject
 import io.realm.annotations.PrimaryKey
 import org.matrix.android.sdk.api.session.room.model.Membership
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
+import org.matrix.android.sdk.internal.database.query.findRootOrLatest
+import org.matrix.android.sdk.internal.extensions.assertIsManaged
 
 internal open class RoomEntity(@PrimaryKey var roomId: String = "",
                                var chunks: RealmList<ChunkEntity> = RealmList(),
                                var sendingTimelineEvents: RealmList<TimelineEventEntity> = RealmList(),
+                               var threadSummaries: RealmList<ThreadSummaryEntity> = RealmList(),
                                var accountData: RealmList<RoomAccountDataEntity> = RealmList()
 ) : RealmObject() {
 
@@ -46,3 +50,10 @@ internal open class RoomEntity(@PrimaryKey var roomId: String = "",
         }
     companion object
 }
+internal fun RoomEntity.removeThreadSummaryIfNeeded(eventId: String) {
+    assertIsManaged()
+    threadSummaries.findRootOrLatest(eventId)?.let {
+        threadSummaries.remove(it)
+        it.deleteFromRealm()
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt
index c0907779721..d0d23dd491b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.database.model
 
 import io.realm.annotations.RealmModule
 import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
 
 /**
  * Realm module for Session
@@ -66,6 +67,7 @@ import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntit
             RoomAccountDataEntity::class,
             SpaceChildSummaryEntity::class,
             SpaceParentSummaryEntity::class,
-            UserPresenceEntity::class
+            UserPresenceEntity::class,
+            ThreadSummaryEntity::class
         ])
 internal class SessionRealmModule
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt
new file mode 100644
index 00000000000..21a80502e72
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2022 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.database.model.threads
+
+import io.realm.RealmObject
+import io.realm.RealmResults
+import io.realm.annotations.Index
+import io.realm.annotations.LinkingObjects
+import org.matrix.android.sdk.internal.database.model.EventEntity
+import org.matrix.android.sdk.internal.database.model.RoomEntity
+
+internal open class ThreadSummaryEntity(@Index var rootThreadEventId: String = "",
+                                        var rootThreadEventEntity: EventEntity? = null,
+                                        var latestThreadEventEntity: EventEntity? = null,
+                                        var rootThreadSenderName: String? = null,
+                                        var latestThreadSenderName: String? = null,
+                                        var rootThreadSenderAvatar: String? = null,
+                                        var latestThreadSenderAvatar: String? = null,
+                                        var rootThreadIsUniqueDisplayName: Boolean = false,
+                                        var isUserParticipating: Boolean = false,
+                                        var latestThreadIsUniqueDisplayName: Boolean = false,
+                                        var numberOfThreads: Int = 0
+) : RealmObject() {
+
+    @LinkingObjects("threadSummaries")
+    val room: RealmResults<RoomEntity>? = null
+
+    companion object
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ThreadSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ThreadSummaryEntityQueries.kt
new file mode 100644
index 00000000000..517d43d7cf9
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ThreadSummaryEntityQueries.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2022 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.database.query
+
+import io.realm.Realm
+import io.realm.RealmList
+import io.realm.RealmQuery
+import io.realm.kotlin.createObject
+import io.realm.kotlin.where
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields
+
+internal fun ThreadSummaryEntity.Companion.where(realm: Realm, roomId: String): RealmQuery<ThreadSummaryEntity> {
+    return realm.where<ThreadSummaryEntity>()
+            .equalTo(ThreadSummaryEntityFields.ROOM.ROOM_ID, roomId)
+}
+
+internal fun ThreadSummaryEntity.Companion.where(realm: Realm, roomId: String, rootThreadEventId: String): RealmQuery<ThreadSummaryEntity> {
+    return where(realm, roomId)
+            .equalTo(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ID, rootThreadEventId)
+}
+
+internal fun ThreadSummaryEntity.Companion.getOrCreate(realm: Realm, roomId: String, rootThreadEventId: String): ThreadSummaryEntity {
+    return where(realm, roomId, rootThreadEventId).findFirst() ?: realm.createObject<ThreadSummaryEntity>().apply {
+        this.rootThreadEventId = rootThreadEventId
+    }
+}
+internal fun ThreadSummaryEntity.Companion.getOrNull(realm: Realm, roomId: String, rootThreadEventId: String): ThreadSummaryEntity? {
+    return where(realm, roomId, rootThreadEventId).findFirst()
+}
+internal fun RealmList<ThreadSummaryEntity>.find(rootThreadEventId: String): ThreadSummaryEntity? {
+    return this.where()
+            .equalTo(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ID, rootThreadEventId)
+            .findFirst()
+}
+
+internal fun RealmList<ThreadSummaryEntity>.findRootOrLatest(eventId: String): ThreadSummaryEntity? {
+    return this.where()
+            .beginGroup()
+            .equalTo(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ID, eventId)
+            .or()
+            .equalTo(ThreadSummaryEntityFields.LATEST_THREAD_EVENT_ENTITY.EVENT_ID, eventId)
+            .endGroup()
+            .findFirst()
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt
index 7415b988a43..2e52354037c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt
@@ -17,9 +17,21 @@
 package org.matrix.android.sdk.internal.session.filter
 
 import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.RelationType
+import timber.log.Timber
 
 internal object FilterFactory {
 
+    fun createThreadsFilter(numberOfEvents: Int, userId: String?): RoomEventFilter {
+        Timber.i("$userId")
+        return RoomEventFilter(
+                limit = numberOfEvents,
+//                senders = listOf(userId),
+//                relationSenders = userId?.let { listOf(it) },
+                relationTypes = listOf(RelationType.IO_THREAD)
+        )
+    }
+
     fun createUploadsFilter(numberOfEvents: Int): RoomEventFilter {
         return RoomEventFilter(
                 limit = numberOfEvents,
@@ -58,8 +70,8 @@ internal object FilterFactory {
 
     private fun createElementTimelineFilter(): RoomEventFilter? {
         return null // RoomEventFilter().apply {
-            // TODO Enable this for optimization
-            // types = listOfSupportedEventTypes.toMutableList()
+        // TODO Enable this for optimization
+        // types = listOfSupportedEventTypes.toMutableList()
         // }
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt
index f4983229670..c93f6a10db2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt
@@ -52,12 +52,15 @@ data class RoomEventFilter(
          * A list of relation types which must be exist pointing to the event being filtered.
          * If this list is absent then no filtering is done on relation types.
          */
-        @Json(name = "relation_types") val relationTypes: List<String>? = null,
+//        @Json(name = "relation_types") val relationTypes: List<String>? = null,
+        @Json(name = "io.element.relation_types") val relationTypes: List<String>? = null,      // To be replaced with the above line after the release
         /**
          *  A list of senders of relations which must exist pointing to the event being filtered.
          *  If this list is absent then no filtering is done on relation types.
          */
-        @Json(name = "relation_senders") val relationSenders: List<String>? = null,
+//        @Json(name = "relation_senders") val relationSenders: List<String>? = null,
+        @Json(name = "io.element.relation_senders") val relationSenders: List<String>? = null, // To be replaced with the above line after the release
+
         /**
          * A list of room IDs to include. If this list is absent then all rooms are included.
          */
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt
index 830a58cd128..55526b41db6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt
@@ -65,7 +65,13 @@ internal data class Capabilities(
          * Clients should make use of this capability to determine if users need to be encouraged to upgrade their rooms.
          */
         @Json(name = "m.room_versions")
-        val roomVersions: RoomVersions? = null
+        val roomVersions: RoomVersions? = null,
+        /**
+         * Capability to indicate if the server supports MSC3440 Threading
+         * True if the user can use m.thread relation, false otherwise
+         */
+        @Json(name = "m.thread")
+        val threads: BooleanCapability? = null
 )
 
 @JsonClass(generateAdapter = true)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
index e822cbdcdbf..8c6bb626d1c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
@@ -121,6 +121,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
                 homeServerCapabilitiesEntity.roomVersionsJson = capabilities?.roomVersions?.let {
                     MoshiProvider.providesMoshi().adapter(RoomVersions::class.java).toJson(it)
                 }
+                homeServerCapabilitiesEntity.canUseThreading = capabilities?.threads?.enabled.orTrue()
             }
 
             if (getMediaConfigResult != null) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt
index 2d8c3e9c78e..34e859e5093 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt
@@ -36,6 +36,7 @@ import org.matrix.android.sdk.api.session.room.send.SendService
 import org.matrix.android.sdk.api.session.room.state.StateService
 import org.matrix.android.sdk.api.session.room.tags.TagsService
 import org.matrix.android.sdk.api.session.room.threads.ThreadsService
+import org.matrix.android.sdk.api.session.room.threads.local.ThreadsLocalService
 import org.matrix.android.sdk.api.session.room.timeline.TimelineService
 import org.matrix.android.sdk.api.session.room.typing.TypingService
 import org.matrix.android.sdk.api.session.room.uploads.UploadsService
@@ -56,6 +57,7 @@ internal class DefaultRoom(override val roomId: String,
                            private val roomSummaryDataSource: RoomSummaryDataSource,
                            private val timelineService: TimelineService,
                            private val threadsService: ThreadsService,
+                           private val threadsLocalService: ThreadsLocalService,
                            private val sendService: SendService,
                            private val draftService: DraftService,
                            private val stateService: StateService,
@@ -80,6 +82,7 @@ internal class DefaultRoom(override val roomId: String,
         Room,
         TimelineService by timelineService,
         ThreadsService by threadsService,
+        ThreadsLocalService by threadsLocalService,
         SendService by sendService,
         DraftService by draftService,
         StateService by stateService,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
index 2eebb70bdcf..d17d16a82db 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
@@ -197,6 +197,16 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                             handleReaction(realm, event, roomId, isLocalEcho)
                         }
                     }
+                    // TODO is that ok??
+//                    else if (event.unsignedData?.relations?.annotations != null) {
+//                        Timber.v("###REACTION e2e Aggregation in room $roomId for event ${event.eventId}")
+//                        handleInitialAggregatedRelations(realm, event, roomId, event.unsignedData.relations.annotations)
+// //                        EventAnnotationsSummaryEntity.where(realm, roomId, event.eventId ?: "").findFirst()
+// //                                ?.let {
+// //                                    TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId ?: "").findAll()
+// //                                            ?.forEach { tet -> tet.annotations = it }
+// //                                }
+//                    }
                 }
                 EventType.REDACTION            -> {
                     val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() }
@@ -244,7 +254,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
     }
 
     // OPT OUT serer aggregation until API mature enough
-    private val SHOULD_HANDLE_SERVER_AGREGGATION = false
+    private val SHOULD_HANDLE_SERVER_AGREGGATION = false // should be true to work with e2e
 
     private fun handleReplace(realm: Realm,
                               event: Event,
@@ -346,6 +356,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
 
     /**
      * Check if the edition is on the latest thread event, and update it accordingly
+     * @param editedEvent The event that will be changed
+     * @param replaceEvent The new event
      */
     private fun handleThreadSummaryEdition(editedEvent: EventEntity?,
                                            replaceEvent: TimelineEventEntity?,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
index 86929e013f3..71838ab5a29 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
@@ -86,7 +86,7 @@ internal interface RoomAPI {
     suspend fun getRoomMessagesFrom(@Path("roomId") roomId: String,
                                     @Query("from") from: String,
                                     @Query("dir") dir: String,
-                                    @Query("limit") limit: Int,
+                                    @Query("limit") limit: Int?,
                                     @Query("filter") filter: String?
     ): PaginationResponse
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt
index 70c1ab4f424..72a3f9ab220 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt
@@ -36,6 +36,7 @@ import org.matrix.android.sdk.internal.session.room.state.SendStateTask
 import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
 import org.matrix.android.sdk.internal.session.room.tags.DefaultTagsService
 import org.matrix.android.sdk.internal.session.room.threads.DefaultThreadsService
+import org.matrix.android.sdk.internal.session.room.threads.local.DefaultThreadsLocalService
 import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimelineService
 import org.matrix.android.sdk.internal.session.room.typing.DefaultTypingService
 import org.matrix.android.sdk.internal.session.room.uploads.DefaultUploadsService
@@ -52,6 +53,7 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService:
                                                       private val roomSummaryDataSource: RoomSummaryDataSource,
                                                       private val timelineServiceFactory: DefaultTimelineService.Factory,
                                                       private val threadsServiceFactory: DefaultThreadsService.Factory,
+                                                      private val threadsLocalServiceFactory: DefaultThreadsLocalService.Factory,
                                                       private val sendServiceFactory: DefaultSendService.Factory,
                                                       private val draftServiceFactory: DefaultDraftService.Factory,
                                                       private val stateServiceFactory: DefaultStateService.Factory,
@@ -79,6 +81,7 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService:
                 roomSummaryDataSource = roomSummaryDataSource,
                 timelineService = timelineServiceFactory.create(roomId),
                 threadsService = threadsServiceFactory.create(roomId),
+                threadsLocalService = threadsLocalServiceFactory.create(roomId),
                 sendService = sendServiceFactory.create(roomId),
                 draftService = draftServiceFactory.create(roomId),
                 stateService = stateServiceFactory.create(roomId),
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt
index f831a77a5d7..5e90076b8af 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt
@@ -77,7 +77,9 @@ import org.matrix.android.sdk.internal.session.room.relation.DefaultUpdateQuickR
 import org.matrix.android.sdk.internal.session.room.relation.FetchEditHistoryTask
 import org.matrix.android.sdk.internal.session.room.relation.FindReactionEventForUndoTask
 import org.matrix.android.sdk.internal.session.room.relation.UpdateQuickReactionTask
+import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadSummariesTask
 import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadTimelineTask
+import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadSummariesTask
 import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
 import org.matrix.android.sdk.internal.session.room.reporting.DefaultReportContentTask
 import org.matrix.android.sdk.internal.session.room.reporting.ReportContentTask
@@ -294,4 +296,7 @@ internal abstract class RoomModule {
 
     @Binds
     abstract fun bindFetchThreadTimelineTask(task: DefaultFetchThreadTimelineTask): FetchThreadTimelineTask
+
+    @Binds
+    abstract fun bindFetchThreadSummariesTask(task: DefaultFetchThreadSummariesTask): FetchThreadSummariesTask
 }
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 d22583e8b7c..f21ee4346cb 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
@@ -37,7 +37,6 @@ import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEnt
 import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
 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.relation.threads.FetchThreadTimelineTask
 import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
 import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
 import org.matrix.android.sdk.internal.util.fetchCopyMap
@@ -51,7 +50,6 @@ internal class DefaultRelationService @AssistedInject constructor(
         private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
         private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
         private val fetchEditHistoryTask: FetchEditHistoryTask,
-        private val fetchThreadTimelineTask: FetchThreadTimelineTask,
         private val timelineEventMapper: TimelineEventMapper,
         @SessionDatabase private val monarchy: Monarchy
 ) : RelationService {
@@ -205,16 +203,6 @@ internal class DefaultRelationService @AssistedInject constructor(
         return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
     }
 
-    override suspend fun fetchThreadTimeline(rootThreadEventId: String): Boolean {
-        fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params(
-                roomId,
-                rootThreadEventId,
-                null,
-                10
-        ))
-        return true
-    }
-
     /**
      * Saves the event in database as a local echo.
      * SendState is set to UNSENT and it's added to a the sendingTimelineEvents list of the room.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt
new file mode 100644
index 00000000000..d316eed691b
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2022 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.room.relation.threads
+
+import com.zhuinden.monarchy.Monarchy
+import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
+import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummaryUpdateType
+import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
+import org.matrix.android.sdk.internal.database.helper.createOrUpdate
+import org.matrix.android.sdk.internal.database.model.RoomEntity
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
+import org.matrix.android.sdk.internal.database.query.where
+import org.matrix.android.sdk.internal.di.SessionDatabase
+import org.matrix.android.sdk.internal.di.UserId
+import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
+import org.matrix.android.sdk.internal.network.executeRequest
+import org.matrix.android.sdk.internal.session.filter.FilterFactory
+import org.matrix.android.sdk.internal.session.room.RoomAPI
+import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
+import org.matrix.android.sdk.internal.session.room.timeline.PaginationResponse
+import org.matrix.android.sdk.internal.task.Task
+import org.matrix.android.sdk.internal.util.awaitTransaction
+import timber.log.Timber
+import javax.inject.Inject
+
+/***
+ * This class is responsible to Fetch all the thread in the current room,
+ * To fetch all threads in a room, the /messages API is used with newly added filtering options.
+ */
+internal interface FetchThreadSummariesTask : Task<FetchThreadSummariesTask.Params, DefaultFetchThreadSummariesTask.Result> {
+    data class Params(
+            val roomId: String,
+            val from: String = "",
+            val limit: Int = 100,
+            val isUserParticipating: Boolean = true
+    )
+}
+
+internal class DefaultFetchThreadSummariesTask @Inject constructor(
+        private val roomAPI: RoomAPI,
+        private val globalErrorReceiver: GlobalErrorReceiver,
+        @SessionDatabase private val monarchy: Monarchy,
+        private val cryptoService: DefaultCryptoService,
+        @UserId private val userId: String,
+) : FetchThreadSummariesTask {
+
+    override suspend fun execute(params: FetchThreadSummariesTask.Params): Result {
+        val filter = FilterFactory.createThreadsFilter(
+                numberOfEvents = params.limit,
+                userId = if (params.isUserParticipating) userId else null).toJSONString()
+
+        val response = executeRequest(
+                globalErrorReceiver,
+                canRetry = true
+        ) {
+            roomAPI.getRoomMessagesFrom(params.roomId, params.from, PaginationDirection.BACKWARDS.value, params.limit, filter)
+        }
+
+        Timber.i("###THREADS DefaultFetchThreadSummariesTask Fetched size:${response.events.size} nextBatch:${response.end} ")
+
+        return handleResponse(response, params)
+    }
+
+    private suspend fun handleResponse(response: PaginationResponse,
+                                       params: FetchThreadSummariesTask.Params): Result {
+        val rootThreadList = response.events
+        monarchy.awaitTransaction { realm ->
+            val roomEntity = RoomEntity.where(realm, roomId = params.roomId).findFirst() ?: return@awaitTransaction
+
+            val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
+            for (rootThreadEvent in rootThreadList) {
+                if (rootThreadEvent.eventId == null || rootThreadEvent.senderId == null || rootThreadEvent.type == null) {
+                    continue
+                }
+
+                ThreadSummaryEntity.createOrUpdate(
+                        threadSummaryType = ThreadSummaryUpdateType.REPLACE,
+                        realm = realm,
+                        roomId = params.roomId,
+                        rootThreadEvent = rootThreadEvent,
+                        roomMemberContentsByUser = roomMemberContentsByUser,
+                        roomEntity = roomEntity,
+                        userId = userId,
+                        cryptoService = cryptoService)
+            }
+        }
+        return Result.SUCCESS
+    }
+
+    enum class Result {
+        SHOULD_FETCH_MORE,
+        REACHED_END,
+        SUCCESS
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
index e7b91ebab74..54ebc620c9d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
@@ -58,7 +58,6 @@ import javax.inject.Inject
 /***
  * This class is responsible to Fetch paginated chunks of the thread timeline using the /relations API
  *
- *
  * How it works
  *
  * The problem?
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt
index 5967ae8d2ed..033c1c0ff9d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt
@@ -23,25 +23,25 @@ import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 import io.realm.Realm
 import org.matrix.android.sdk.api.session.room.threads.ThreadsService
-import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
-import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
-import org.matrix.android.sdk.internal.database.helper.findAllLocalThreadNotificationsForRoomId
+import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
+import org.matrix.android.sdk.internal.database.helper.enhanceWithEditions
 import org.matrix.android.sdk.internal.database.helper.findAllThreadsForRoomId
-import org.matrix.android.sdk.internal.database.helper.isUserParticipatingInThread
-import org.matrix.android.sdk.internal.database.helper.mapEventsWithEdition
+import org.matrix.android.sdk.internal.database.mapper.ThreadSummaryMapper
 import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
-import org.matrix.android.sdk.internal.database.model.EventEntity
-import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
-import org.matrix.android.sdk.internal.database.query.where
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
 import org.matrix.android.sdk.internal.di.SessionDatabase
 import org.matrix.android.sdk.internal.di.UserId
-import org.matrix.android.sdk.internal.util.awaitTransaction
+import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadSummariesTask
+import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
 
 internal class DefaultThreadsService @AssistedInject constructor(
         @Assisted private val roomId: String,
         @UserId private val userId: String,
+        private val fetchThreadTimelineTask: FetchThreadTimelineTask,
+        private val fetchThreadSummariesTask: FetchThreadSummariesTask,
         @SessionDatabase private val monarchy: Monarchy,
         private val timelineEventMapper: TimelineEventMapper,
+        private val threadSummaryMapper: ThreadSummaryMapper
 ) : ThreadsService {
 
     @AssistedFactory
@@ -49,55 +49,40 @@ internal class DefaultThreadsService @AssistedInject constructor(
         fun create(roomId: String): DefaultThreadsService
     }
 
-    override fun getMarkedThreadNotificationsLive(): LiveData<List<TimelineEvent>> {
+    override fun getAllThreadSummariesLive(): LiveData<List<ThreadSummary>> {
         return monarchy.findAllMappedWithChanges(
-                { TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) },
-                { timelineEventMapper.map(it) }
+                { ThreadSummaryEntity.findAllThreadsForRoomId(it, roomId = roomId) },
+                {
+                    threadSummaryMapper.map(it)
+                }
         )
     }
 
-    override fun getMarkedThreadNotifications(): List<TimelineEvent> {
+    override fun getAllThreadSummaries(): List<ThreadSummary> {
         return monarchy.fetchAllMappedSync(
-                { TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) },
-                { timelineEventMapper.map(it) }
+                { ThreadSummaryEntity.findAllThreadsForRoomId(it, roomId = roomId) },
+                { threadSummaryMapper.map(it) }
         )
     }
 
-    override fun getAllThreadsLive(): LiveData<List<TimelineEvent>> {
-        return monarchy.findAllMappedWithChanges(
-                { TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) },
-                { timelineEventMapper.map(it) }
-        )
-    }
-
-    override fun getAllThreads(): List<TimelineEvent> {
-        return monarchy.fetchAllMappedSync(
-                { TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) },
-                { timelineEventMapper.map(it) }
-        )
-    }
-
-    override fun isUserParticipatingInThread(rootThreadEventId: String): Boolean {
+    override fun enhanceWithEditions(threads: List<ThreadSummary>): List<ThreadSummary> {
         return Realm.getInstance(monarchy.realmConfiguration).use {
-            TimelineEventEntity.isUserParticipatingInThread(
-                    realm = it,
-                    roomId = roomId,
-                    rootThreadEventId = rootThreadEventId,
-                    senderId = userId)
+            threads.enhanceWithEditions(it, roomId)
         }
     }
 
-    override fun mapEventsWithEdition(threads: List<TimelineEvent>): List<TimelineEvent> {
-        return Realm.getInstance(monarchy.realmConfiguration).use {
-            threads.mapEventsWithEdition(it, roomId)
-        }
+    override suspend fun fetchThreadTimeline(rootThreadEventId: String, from: String, limit: Int) {
+        fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params(
+                roomId = roomId,
+                rootThreadEventId = rootThreadEventId,
+                from = from,
+                limit = limit
+        ))
     }
 
-    override suspend fun markThreadAsRead(rootThreadEventId: String) {
-        monarchy.awaitTransaction {
-            EventEntity.where(
-                    realm = it,
-                    eventId = rootThreadEventId).findFirst()?.threadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE
-        }
+    override suspend fun fetchThreadSummaries() {
+        fetchThreadSummariesTask.execute(FetchThreadSummariesTask.Params(
+                roomId = roomId
+        ))
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/local/DefaultThreadsLocalService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/local/DefaultThreadsLocalService.kt
new file mode 100644
index 00000000000..3bc36fb2a80
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/local/DefaultThreadsLocalService.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2022 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.room.threads.local
+
+import androidx.lifecycle.LiveData
+import com.zhuinden.monarchy.Monarchy
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import io.realm.Realm
+import org.matrix.android.sdk.api.session.room.threads.local.ThreadsLocalService
+import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
+import org.matrix.android.sdk.internal.database.helper.findAllLocalThreadNotificationsForRoomId
+import org.matrix.android.sdk.internal.database.helper.findAllThreadsForRoomId
+import org.matrix.android.sdk.internal.database.helper.isUserParticipatingInThread
+import org.matrix.android.sdk.internal.database.helper.mapEventsWithEdition
+import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
+import org.matrix.android.sdk.internal.database.model.EventEntity
+import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
+import org.matrix.android.sdk.internal.database.query.where
+import org.matrix.android.sdk.internal.di.SessionDatabase
+import org.matrix.android.sdk.internal.di.UserId
+import org.matrix.android.sdk.internal.util.awaitTransaction
+
+internal class DefaultThreadsLocalService @AssistedInject constructor(
+        @Assisted private val roomId: String,
+        @UserId private val userId: String,
+        @SessionDatabase private val monarchy: Monarchy,
+        private val timelineEventMapper: TimelineEventMapper,
+) : ThreadsLocalService {
+
+    @AssistedFactory
+    interface Factory {
+        fun create(roomId: String): DefaultThreadsLocalService
+    }
+
+    override fun getMarkedThreadNotificationsLive(): LiveData<List<TimelineEvent>> {
+        return monarchy.findAllMappedWithChanges(
+                { TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) },
+                { timelineEventMapper.map(it) }
+        )
+    }
+
+    override fun getMarkedThreadNotifications(): List<TimelineEvent> {
+        return monarchy.fetchAllMappedSync(
+                { TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) },
+                { timelineEventMapper.map(it) }
+        )
+    }
+
+    override fun getAllThreadsLive(): LiveData<List<TimelineEvent>> {
+        return monarchy.findAllMappedWithChanges(
+                { TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) },
+                { timelineEventMapper.map(it) }
+        )
+    }
+
+    override fun getAllThreads(): List<TimelineEvent> {
+        return monarchy.fetchAllMappedSync(
+                { TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) },
+                { timelineEventMapper.map(it) }
+        )
+    }
+
+    override fun isUserParticipatingInThread(rootThreadEventId: String): Boolean {
+        return Realm.getInstance(monarchy.realmConfiguration).use {
+            TimelineEventEntity.isUserParticipatingInThread(
+                    realm = it,
+                    roomId = roomId,
+                    rootThreadEventId = rootThreadEventId,
+                    senderId = userId)
+        }
+    }
+
+    override fun mapEventsWithEdition(threads: List<TimelineEvent>): List<TimelineEvent> {
+        return Realm.getInstance(monarchy.realmConfiguration).use {
+            threads.mapEventsWithEdition(it, roomId)
+        }
+    }
+
+    override suspend fun markThreadAsRead(rootThreadEventId: String) {
+        monarchy.awaitTransaction {
+            EventEntity.where(
+                    realm = it,
+                    eventId = rootThreadEventId).findFirst()?.threadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE
+        }
+    }
+}
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 573af7c696f..63857d611b9 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
@@ -23,10 +23,12 @@ 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.EventType
 import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
 import org.matrix.android.sdk.api.session.initsync.InitSyncStep
 import org.matrix.android.sdk.api.session.room.model.Membership
 import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
 import org.matrix.android.sdk.api.session.room.send.SendState
+import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummaryUpdateType
 import org.matrix.android.sdk.api.session.sync.model.InvitedRoomSync
 import org.matrix.android.sdk.api.session.sync.model.LazyRoomSyncEphemeral
 import org.matrix.android.sdk.api.session.sync.model.RoomSync
@@ -36,6 +38,7 @@ import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
 import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
 import org.matrix.android.sdk.internal.database.helper.addIfNecessary
 import org.matrix.android.sdk.internal.database.helper.addTimelineEvent
+import org.matrix.android.sdk.internal.database.helper.createOrUpdate
 import org.matrix.android.sdk.internal.database.helper.updateThreadSummaryIfNeeded
 import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
 import org.matrix.android.sdk.internal.database.mapper.asDomain
@@ -48,6 +51,7 @@ import org.matrix.android.sdk.internal.database.model.RoomEntity
 import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
 import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
 import org.matrix.android.sdk.internal.database.model.deleteOnCascade
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
 import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
 import org.matrix.android.sdk.internal.database.query.find
 import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
@@ -60,6 +64,7 @@ import org.matrix.android.sdk.internal.di.UserId
 import org.matrix.android.sdk.internal.extensions.clearWith
 import org.matrix.android.sdk.internal.session.StreamEventsManager
 import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent
+import org.matrix.android.sdk.internal.session.homeserver.DefaultHomeServerCapabilitiesService
 import org.matrix.android.sdk.internal.session.initsync.ProgressReporter
 import org.matrix.android.sdk.internal.session.initsync.mapWithProgress
 import org.matrix.android.sdk.internal.session.initsync.reportSubtask
@@ -86,6 +91,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                                                    private val threadsAwarenessHandler: ThreadsAwarenessHandler,
                                                    private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
                                                    @UserId private val userId: String,
+                                                   private val homeServerCapabilitiesService: HomeServerCapabilitiesService,
                                                    private val lightweightSettingsStorage: LightweightSettingsStorage,
                                                    private val timelineInput: TimelineInput,
                                                    private val liveEventService: Lazy<StreamEventsManager>) {
@@ -345,7 +351,6 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
         return roomEntity
     }
 
-    val customList = arrayListOf<String>()
     private fun handleTimelineEvents(realm: Realm,
                                      roomId: String,
                                      roomEntity: RoomEntity,
@@ -420,6 +425,17 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                     optimizedThreadSummaryMap[it] = eventEntity
                     // Add the same thread timeline event to Thread Chunk
                     addToThreadChunkIfNeeded(realm, roomId, it, timelineEventAdded, roomEntity)
+                    // Add thread list if needed
+                    if(homeServerCapabilitiesService.getHomeServerCapabilities().canUseThreading) {
+                        ThreadSummaryEntity.createOrUpdate(
+                                threadSummaryType = ThreadSummaryUpdateType.ADD,
+                                realm = realm,
+                                roomId = roomId,
+                                threadEventEntity = eventEntity,
+                                roomMemberContentsByUser = roomMemberContentsByUser,
+                                userId = userId,
+                                roomEntity = roomEntity)
+                    }
                 } ?: run {
                     // This is a normal event or a root thread one
                     optimizedThreadSummaryMap[eventEntity.eventId] = eventEntity
diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt
index ca18060c51b..fc76535c4c7 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt
@@ -32,7 +32,6 @@ import im.vector.app.features.home.room.detail.arguments.TimelineArgs
 import im.vector.app.features.home.room.threads.arguments.ThreadListArgs
 import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs
 import im.vector.app.features.home.room.threads.list.views.ThreadListFragment
-import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
 import javax.inject.Inject
 
 @AndroidEntryPoint
@@ -92,14 +91,7 @@ class ThreadsActivity : VectorBaseActivity<ActivityThreadsBinding>() {
      * This function is used to navigate to the selected thread timeline.
      * One usage of that is from the Threads Activity
      */
-    fun navigateToThreadTimeline(
-            timelineEvent: TimelineEvent) {
-        val roomThreadDetailArgs = ThreadTimelineArgs(
-                roomId = timelineEvent.roomId,
-                displayName = timelineEvent.senderInfo.displayName,
-                avatarUrl = timelineEvent.senderInfo.avatarUrl,
-                roomEncryptionTrustLevel = null,
-                rootThreadEventId = timelineEvent.eventId)
+    fun navigateToThreadTimeline(threadTimelineArgs: ThreadTimelineArgs) {
         val commonOption: (FragmentTransaction) -> Unit = {
             it.setCustomAnimations(
                     R.anim.animation_slide_in_right,
@@ -111,8 +103,8 @@ class ThreadsActivity : VectorBaseActivity<ActivityThreadsBinding>() {
                 container = views.threadsActivityFragmentContainer,
                 fragmentClass = TimelineFragment::class.java,
                 params = TimelineArgs(
-                        roomId = timelineEvent.roomId,
-                        threadTimelineArgs = roomThreadDetailArgs
+                        roomId = threadTimelineArgs.roomId,
+                        threadTimelineArgs = threadTimelineArgs
                 ),
                 option = commonOption
         )
diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt
index 32cb0068102..d3a5497d63b 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt
@@ -23,15 +23,19 @@ import im.vector.app.core.date.VectorDateFormatter
 import im.vector.app.core.resources.StringProvider
 import im.vector.app.features.home.AvatarRenderer
 import im.vector.app.features.home.room.threads.list.model.threadListItem
+import org.matrix.android.sdk.api.session.Session
+import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
 import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
 import org.matrix.android.sdk.api.util.toMatrixItem
+import org.matrix.android.sdk.api.util.toMatrixItemOrNull
 import javax.inject.Inject
 
 class ThreadListController @Inject constructor(
         private val avatarRenderer: AvatarRenderer,
         private val stringProvider: StringProvider,
-        private val dateFormatter: VectorDateFormatter
+        private val dateFormatter: VectorDateFormatter,
+        private val session: Session
 ) : EpoxyController() {
 
     var listener: Listener? = null
@@ -43,10 +47,59 @@ class ThreadListController @Inject constructor(
         requestModelBuild()
     }
 
-    override fun buildModels() {
+    override fun buildModels() =
+            when (session.getHomeServerCapabilities().canUseThreading) {
+                true  -> buildThreadSummaries()
+                false -> buildThreadList()
+            }
+
+    /**
+     * Building thread summaries when homeserver
+     * supports threading
+     */
+    private fun buildThreadSummaries() {
         val safeViewState = viewState ?: return
         val host = this
+        safeViewState.threadSummaryList.invoke()
+                ?.filter {
+                    if (safeViewState.shouldFilterThreads) {
+                        it.isUserParticipating
+                    } else {
+                        true
+                    }
+                }
+                ?.forEach { threadSummary ->
+                    val date = dateFormatter.format(threadSummary.latestEvent?.originServerTs, DateFormatKind.ROOM_LIST)
+                    val decryptionErrorMessage = stringProvider.getString(R.string.encrypted_message)
+                    val rootThreadEdition = threadSummary.threadEditions.rootThreadEdition
+                    val latestThreadEdition = threadSummary.threadEditions.latestThreadEdition
+                    threadListItem {
+                        id(threadSummary.rootEvent?.eventId)
+                        avatarRenderer(host.avatarRenderer)
+                        matrixItem(threadSummary.rootThreadSenderInfo.toMatrixItem())
+                        title(threadSummary.rootThreadSenderInfo.displayName.orEmpty())
+                        date(date)
+                        rootMessageDeleted(threadSummary.rootEvent?.isRedacted() ?: false)
+                        // TODO refactor notifications that with the new thread summary
+                        threadNotificationState(threadSummary.rootEvent?.threadDetails?.threadNotificationState ?: ThreadNotificationState.NO_NEW_MESSAGE)
+                        rootMessage(rootThreadEdition ?: threadSummary.rootEvent?.getDecryptedTextSummary() ?: decryptionErrorMessage)
+                        lastMessage(latestThreadEdition ?: threadSummary.latestEvent?.getDecryptedTextSummary() ?: decryptionErrorMessage)
+                        lastMessageCounter(threadSummary.numberOfThreads.toString())
+                        lastMessageMatrixItem(threadSummary.latestThreadSenderInfo.toMatrixItemOrNull())
+                        itemClickListener {
+                            host.listener?.onThreadSummaryClicked(threadSummary)
+                        }
+                    }
+                }
+    }
 
+    /**
+     * Building local thread list when homeserver do not
+     * support threading
+     */
+    private fun buildThreadList() {
+        val safeViewState = viewState ?: return
+        val host = this
         safeViewState.rootThreadEventList.invoke()
                 ?.filter {
                     if (safeViewState.shouldFilterThreads) {
@@ -74,13 +127,14 @@ class ThreadListController @Inject constructor(
                         lastMessageCounter(timelineEvent.root.threadDetails?.numberOfThreads.toString())
                         lastMessageMatrixItem(timelineEvent.root.threadDetails?.threadSummarySenderInfo?.toMatrixItem())
                         itemClickListener {
-                            host.listener?.onThreadClicked(timelineEvent)
+                            host.listener?.onThreadListClicked(timelineEvent)
                         }
                     }
                 }
     }
 
     interface Listener {
-        fun onThreadClicked(timelineEvent: TimelineEvent)
+        fun onThreadSummaryClicked(threadSummary: ThreadSummary)
+        fun onThreadListClicked(timelineEvent: TimelineEvent)
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt
index d82b5d6ccf5..290b71a5041 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt
@@ -28,6 +28,7 @@ import im.vector.app.core.platform.VectorViewModel
 import im.vector.app.features.home.room.threads.list.views.ThreadListFragment
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
 import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.api.session.threads.ThreadTimelineEvent
 import org.matrix.android.sdk.flow.flow
@@ -53,11 +54,41 @@ class ThreadListViewModel @AssistedInject constructor(@Assisted val initialState
     }
 
     init {
-        observeThreadsList()
+        observeThreads()
+        fetchThreadList()
     }
 
     override fun handle(action: EmptyAction) {}
 
+    /**
+     * Observing thread list with respect to homeserver
+     * capabilities
+     */
+    private fun observeThreads() {
+        when (session.getHomeServerCapabilities().canUseThreading) {
+            true  -> observeThreadSummaries()
+            false -> observeThreadsList()
+        }
+    }
+
+    /**
+     * Observing thread summaries when homeserver support
+     * threading
+     */
+    private fun observeThreadSummaries() {
+        room?.flow()
+                ?.liveThreadSummaries()
+                ?.map { room.enhanceWithEditions(it) }
+                ?.flowOn(room.coroutineDispatchers.io)
+                ?.execute { asyncThreads ->
+                    copy(threadSummaryList = asyncThreads)
+                }
+    }
+
+    /**
+     * Observing thread list when homeserver do not support
+     * threading
+     */
     private fun observeThreadsList() {
         room?.flow()
                 ?.liveThreadList()
@@ -74,6 +105,14 @@ class ThreadListViewModel @AssistedInject constructor(@Assisted val initialState
                 }
     }
 
+    private fun fetchThreadList() {
+        viewModelScope.launch {
+            room?.fetchThreadSummaries()
+        }
+    }
+
+    fun canHomeserverUseThreading() =  session.getHomeServerCapabilities().canUseThreading
+
     fun applyFiltering(shouldFilterThreads: Boolean) {
         setState {
             copy(shouldFilterThreads = shouldFilterThreads)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewState.kt
index 2a70a5be1e6..e08f70030bb 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewState.kt
@@ -20,13 +20,14 @@ import com.airbnb.mvrx.Async
 import com.airbnb.mvrx.MavericksState
 import com.airbnb.mvrx.Uninitialized
 import im.vector.app.features.home.room.threads.arguments.ThreadListArgs
+import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
 import org.matrix.android.sdk.api.session.threads.ThreadTimelineEvent
 
 data class ThreadListViewState(
+        val threadSummaryList: Async<List<ThreadSummary>> = Uninitialized,
         val rootThreadEventList: Async<List<ThreadTimelineEvent>> = Uninitialized,
         val shouldFilterThreads: Boolean = false,
         val roomId: String
 ) : MavericksState {
-
     constructor(args: ThreadListArgs) : this(roomId = args.roomId)
 }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt
index 180e6226d00..949778629bc 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt
@@ -34,9 +34,11 @@ import im.vector.app.features.home.AvatarRenderer
 import im.vector.app.features.home.room.detail.timeline.animation.TimelineItemAnimator
 import im.vector.app.features.home.room.threads.ThreadsActivity
 import im.vector.app.features.home.room.threads.arguments.ThreadListArgs
+import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs
 import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListController
 import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewModel
 import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewState
+import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
 import org.matrix.android.sdk.api.util.MatrixItem
 import javax.inject.Inject
@@ -111,12 +113,30 @@ class ThreadListFragment @Inject constructor(
         views.includeThreadListToolbar.roomToolbarThreadSubtitleTextView.text = threadListArgs.displayName
     }
 
-    override fun onThreadClicked(timelineEvent: TimelineEvent) {
-        (activity as? ThreadsActivity)?.navigateToThreadTimeline(timelineEvent)
+    override fun onThreadSummaryClicked(threadSummary: ThreadSummary) {
+        val roomThreadDetailArgs = ThreadTimelineArgs(
+                roomId = threadSummary.roomId,
+                displayName = threadSummary.rootThreadSenderInfo.displayName,
+                avatarUrl = threadSummary.rootThreadSenderInfo.avatarUrl,
+                roomEncryptionTrustLevel = null,
+                rootThreadEventId = threadSummary.rootEventId)
+        (activity as? ThreadsActivity)?.navigateToThreadTimeline(roomThreadDetailArgs)
+    }
+
+    override fun onThreadListClicked(timelineEvent: TimelineEvent) {
+        val threadTimelineArgs = ThreadTimelineArgs(
+                roomId = timelineEvent.roomId,
+                displayName = timelineEvent.senderInfo.displayName,
+                avatarUrl = timelineEvent.senderInfo.avatarUrl,
+                roomEncryptionTrustLevel = null,
+                rootThreadEventId = timelineEvent.eventId)
+        (activity as? ThreadsActivity)?.navigateToThreadTimeline(threadTimelineArgs)
     }
 
     private fun renderEmptyStateIfNeeded(state: ThreadListViewState) {
-        val show = state.rootThreadEventList.invoke().isNullOrEmpty()
-        views.threadListEmptyConstraintLayout.isVisible = show
+        when (threadListViewModel.canHomeserverUseThreading()) {
+            true  -> views.threadListEmptyConstraintLayout.isVisible = state.threadSummaryList.invoke().isNullOrEmpty()
+            false -> views.threadListEmptyConstraintLayout.isVisible = state.rootThreadEventList.invoke().isNullOrEmpty()
+        }
     }
 }

From f4f48b919e13367413f5211b77bd5e5652d85db2 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Mon, 21 Feb 2022 12:14:51 +0200
Subject: [PATCH 07/38] Improve home server capabilities for threads

---
 .../internal/database/mapper/HomeServerCapabilitiesMapper.kt | 2 +-
 .../session/homeserver/GetHomeServerCapabilitiesTask.kt      | 3 ++-
 .../internal/session/sync/handler/room/RoomSyncHandler.kt    | 5 ++---
 3 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt
index 8be3455c079..2e33988a22b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt
@@ -42,7 +42,7 @@ internal object HomeServerCapabilitiesMapper {
                 lastVersionIdentityServerSupported = entity.lastVersionIdentityServerSupported,
                 defaultIdentityServerUrl = entity.defaultIdentityServerUrl,
                 roomVersions = mapRoomVersion(entity.roomVersionsJson),
-                canUseThreading = false // entity.canUseThreading
+                canUseThreading = entity.canUseThreading
         )
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
index 8c6bb626d1c..ae8e31c8a16 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
@@ -20,6 +20,7 @@ import com.zhuinden.monarchy.Monarchy
 import org.matrix.android.sdk.api.MatrixPatterns.getDomain
 import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
 import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
+import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.extensions.orTrue
 import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
 import org.matrix.android.sdk.internal.auth.version.Versions
@@ -121,7 +122,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
                 homeServerCapabilitiesEntity.roomVersionsJson = capabilities?.roomVersions?.let {
                     MoshiProvider.providesMoshi().adapter(RoomVersions::class.java).toJson(it)
                 }
-                homeServerCapabilitiesEntity.canUseThreading = capabilities?.threads?.enabled.orTrue()
+                homeServerCapabilitiesEntity.canUseThreading = capabilities?.threads?.enabled.orFalse()
             }
 
             if (getMediaConfigResult != null) {
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 63857d611b9..591b3c2ed5c 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
@@ -64,7 +64,6 @@ import org.matrix.android.sdk.internal.di.UserId
 import org.matrix.android.sdk.internal.extensions.clearWith
 import org.matrix.android.sdk.internal.session.StreamEventsManager
 import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent
-import org.matrix.android.sdk.internal.session.homeserver.DefaultHomeServerCapabilitiesService
 import org.matrix.android.sdk.internal.session.initsync.ProgressReporter
 import org.matrix.android.sdk.internal.session.initsync.mapWithProgress
 import org.matrix.android.sdk.internal.session.initsync.reportSubtask
@@ -425,8 +424,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                     optimizedThreadSummaryMap[it] = eventEntity
                     // Add the same thread timeline event to Thread Chunk
                     addToThreadChunkIfNeeded(realm, roomId, it, timelineEventAdded, roomEntity)
-                    // Add thread list if needed
-                    if(homeServerCapabilitiesService.getHomeServerCapabilities().canUseThreading) {
+                    if (homeServerCapabilitiesService.getHomeServerCapabilities().canUseThreading) {
+                        // Update thread summaries only if homeserver supports threading
                         ThreadSummaryEntity.createOrUpdate(
                                 threadSummaryType = ThreadSummaryUpdateType.ADD,
                                 realm = realm,

From 2b740a1ab662965c24909f53d6c52c0f80284836 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Mon, 21 Feb 2022 17:23:17 +0200
Subject: [PATCH 08/38] Implement permalink support for /relations live thread
 timeline

---
 .../session/room/timeline/DefaultTimeline.kt  |  8 ++++++-
 .../session/room/timeline/TimelineChunk.kt    |  3 ++-
 .../home/room/detail/TimelineViewModel.kt     | 23 +++++++++++++++++++
 3 files changed, 32 insertions(+), 2 deletions(-)

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 56629866634..8c2b4d2bbe2 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
@@ -301,7 +301,13 @@ internal class DefaultTimeline(private val roomId: String,
         Timber.v("Post snapshot of ${snapshot.size} events")
         withContext(coroutineDispatchers.main) {
             listeners.forEach {
-                tryOrNull { it.onTimelineUpdated(snapshot) }
+                if (initialEventId != null && isFromThreadTimeline && snapshot.firstOrNull { it.eventId == initialEventId } == null) {
+                    // We are in a thread timeline with a permalink, post update timeline only when the appropriate message have been found
+                    tryOrNull { it.onTimelineUpdated(arrayListOf()) }
+                } else {
+                    // In all the other cases update timeline as expected
+                    tryOrNull { it.onTimelineUpdated(snapshot) }
+                }
             }
         }
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
index 864b3f0dd9b..34aa83f9c90 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
@@ -171,11 +171,12 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
      * always fetch results, while we want our data to be up to dated.
      */
     suspend fun loadMoreThread(count: Int, direction: Timeline.Direction): LoadMoreResult {
+        val rootThreadEventId = timelineSettings.rootThreadEventId ?: return LoadMoreResult.FAILURE
         return if (direction == Timeline.Direction.BACKWARDS) {
             try {
                 fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params(
                         roomId,
-                        timelineSettings.rootThreadEventId!!,
+                        rootThreadEventId,
                         chunkEntity.prevToken,
                         count
                 )).toLoadMoreResult()
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
index a404f9136b8..b882990e30e 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
@@ -156,6 +156,9 @@ class TimelineViewModel @AssistedInject constructor(
 
     companion object : MavericksViewModelFactory<TimelineViewModel, RoomDetailViewState> by hiltMavericksViewModelFactory() {
         const val PAGINATION_COUNT = 50
+        // The larger the number the faster the results, COUNT=200 for 500 thread messages its x4 faster than COUNT=50
+        const val PAGINATION_COUNT_THREADS_PERMALINK = 200
+
     }
 
     init {
@@ -1174,10 +1177,30 @@ class TimelineViewModel @AssistedInject constructor(
         }
     }
 
+    /**
+     * Navigates to the appropriate event (by paginating the thread timeline until the event is found
+     * in the snapshot. The main reason for this function is to support the /relations api
+     */
+    private var threadPermalinkHandled = false
+    private fun navigateToThreadEventIfNeeded(snapshot: List<TimelineEvent>) {
+        if (eventId != null && initialState.rootThreadEventId != null) {
+            // When we have a permalink and we are in a thread timeline
+            if (snapshot.firstOrNull { it.eventId == eventId } != null && !threadPermalinkHandled) {
+                // Permalink event found lets navigate there
+                handleNavigateToEvent(RoomDetailAction.NavigateToEvent(eventId, true))
+                threadPermalinkHandled = true
+            } else {
+                // Permalink event not found yet continue paginating
+                timeline.paginate(Timeline.Direction.BACKWARDS, PAGINATION_COUNT_THREADS_PERMALINK)
+            }
+        }
+    }
+
     override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
         viewModelScope.launch {
             // tryEmit doesn't work with SharedFlow without cache
             timelineEvents.emit(snapshot)
+            navigateToThreadEventIfNeeded(snapshot)
         }
     }
 

From 0f721d971c1fe2e58406598c8d91cf3decb7e814 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Mon, 21 Feb 2022 17:24:34 +0200
Subject: [PATCH 09/38] Ktlint format

---
 .../vector/app/features/home/room/detail/TimelineViewModel.kt   | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
index b882990e30e..a8313324079 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
@@ -156,9 +156,9 @@ class TimelineViewModel @AssistedInject constructor(
 
     companion object : MavericksViewModelFactory<TimelineViewModel, RoomDetailViewState> by hiltMavericksViewModelFactory() {
         const val PAGINATION_COUNT = 50
+
         // The larger the number the faster the results, COUNT=200 for 500 thread messages its x4 faster than COUNT=50
         const val PAGINATION_COUNT_THREADS_PERMALINK = 200
-
     }
 
     init {

From deb86d2e874d00c8e895b4aa7de34266cae8dd69 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Tue, 22 Feb 2022 13:18:09 +0200
Subject: [PATCH 10/38] Resolve real migration conflicts

---
 .../database/RealmSessionStoreMigration.kt    |  4 +-
 .../database/migration/MigrateSessionTo026.kt | 28 +++++++++++
 .../database/migration/MigrateSessionTo027.kt | 49 -------------------
 3 files changed, 29 insertions(+), 52 deletions(-)
 delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo027.kt

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
index 24ac3106532..a57397dad5f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
@@ -44,7 +44,6 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo023
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo024
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo025
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo026
-import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo027
 import org.matrix.android.sdk.internal.util.Normalizer
 import timber.log.Timber
 import javax.inject.Inject
@@ -59,7 +58,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
     override fun equals(other: Any?) = other is RealmSessionStoreMigration
     override fun hashCode() = 1000
 
-    val schemaVersion = 27L
+    val schemaVersion = 26L
 
     override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
         Timber.d("Migrating Realm Session from $oldVersion to $newVersion")
@@ -90,6 +89,5 @@ internal class RealmSessionStoreMigration @Inject constructor(
         if (oldVersion < 24) MigrateSessionTo024(realm).perform()
         if (oldVersion < 25) MigrateSessionTo025(realm).perform()
         if (oldVersion < 26) MigrateSessionTo026(realm).perform()
-        if (oldVersion < 27) MigrateSessionTo027(realm).perform()
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt
index 597d6d1cbe3..04b64c28936 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt
@@ -19,9 +19,17 @@ package org.matrix.android.sdk.internal.database.migration
 import io.realm.DynamicRealm
 import io.realm.FieldAttribute
 import org.matrix.android.sdk.internal.database.model.ChunkEntityFields
+import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
+import org.matrix.android.sdk.internal.database.model.RoomEntityFields
 import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 
+/**
+ * Migrating to:
+ * Live thread list: using enhanced /messages api MSC3440
+ * Live thread timeline: using /relations api
+ */
 class MigrateSessionTo026(realm: DynamicRealm) : RealmMigrator(realm, 26) {
 
     override fun doMigrate(realm: DynamicRealm) {
@@ -31,5 +39,25 @@ class MigrateSessionTo026(realm: DynamicRealm) : RealmMigrator(realm, 26) {
 
         realm.schema.get("TimelineEventEntity")
                 ?.addField(TimelineEventEntityFields.OWNED_BY_THREAD_CHUNK, Boolean::class.java)
+
+        val eventEntity = realm.schema.get("EventEntity") ?: return
+        val threadSummaryEntity = realm.schema.create("ThreadSummaryEntity")
+                .addField(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED)
+                .addField(ThreadSummaryEntityFields.ROOT_THREAD_SENDER_NAME, String::class.java)
+                .addField(ThreadSummaryEntityFields.ROOT_THREAD_SENDER_AVATAR, String::class.java)
+                .addField(ThreadSummaryEntityFields.ROOT_THREAD_IS_UNIQUE_DISPLAY_NAME, String::class.java)
+                .addField(ThreadSummaryEntityFields.LATEST_THREAD_SENDER_NAME, String::class.java)
+                .addField(ThreadSummaryEntityFields.LATEST_THREAD_SENDER_AVATAR, String::class.java)
+                .addField(ThreadSummaryEntityFields.LATEST_THREAD_IS_UNIQUE_DISPLAY_NAME, String::class.java)
+                .addField(ThreadSummaryEntityFields.NUMBER_OF_THREADS, Int::class.java)
+                .addField(ThreadSummaryEntityFields.IS_USER_PARTICIPATING, Boolean::class.java)
+                .addRealmObjectField(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ENTITY.`$`, eventEntity)
+                .addRealmObjectField(ThreadSummaryEntityFields.LATEST_THREAD_EVENT_ENTITY.`$`, eventEntity)
+
+        realm.schema.get("RoomEntity")
+                ?.addRealmListField(RoomEntityFields.THREAD_SUMMARIES.`$`, threadSummaryEntity)
+
+        realm.schema.get("HomeServerCapabilitiesEntity")
+                ?.addRealmListField(HomeServerCapabilitiesEntityFields.CAN_USE_THREADING, Boolean::class.java)
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo027.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo027.kt
deleted file mode 100644
index b56b7d325b4..00000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo027.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright (c) 2022 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.database.migration
-
-import io.realm.DynamicRealm
-import io.realm.FieldAttribute
-import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
-import org.matrix.android.sdk.internal.database.model.RoomEntityFields
-import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields
-import org.matrix.android.sdk.internal.util.database.RealmMigrator
-
-class MigrateSessionTo027(realm: DynamicRealm) : RealmMigrator(realm, 27) {
-
-    override fun doMigrate(realm: DynamicRealm) {
-        val eventEntity = realm.schema.get("EventEntity") ?: return
-        val threadSummaryEntity = realm.schema.create("ThreadSummaryEntity")
-                .addField(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED)
-                .addField(ThreadSummaryEntityFields.ROOT_THREAD_SENDER_NAME, String::class.java)
-                .addField(ThreadSummaryEntityFields.ROOT_THREAD_SENDER_AVATAR, String::class.java)
-                .addField(ThreadSummaryEntityFields.ROOT_THREAD_IS_UNIQUE_DISPLAY_NAME, String::class.java)
-                .addField(ThreadSummaryEntityFields.LATEST_THREAD_SENDER_NAME, String::class.java)
-                .addField(ThreadSummaryEntityFields.LATEST_THREAD_SENDER_AVATAR, String::class.java)
-                .addField(ThreadSummaryEntityFields.LATEST_THREAD_IS_UNIQUE_DISPLAY_NAME, String::class.java)
-                .addField(ThreadSummaryEntityFields.NUMBER_OF_THREADS, Int::class.java)
-                .addField(ThreadSummaryEntityFields.IS_USER_PARTICIPATING, Boolean::class.java)
-                .addRealmObjectField(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ENTITY.`$`, eventEntity)
-                .addRealmObjectField(ThreadSummaryEntityFields.LATEST_THREAD_EVENT_ENTITY.`$`, eventEntity)
-
-        realm.schema.get("RoomEntity")
-                ?.addRealmListField(RoomEntityFields.THREAD_SUMMARIES.`$`, threadSummaryEntity)
-
-        realm.schema.get("HomeServerCapabilitiesEntity")
-                ?.addRealmListField(HomeServerCapabilitiesEntityFields.CAN_USE_THREADING, Boolean::class.java)
-    }
-}

From 9953d0d0ed095f6c144e80e9ac0fbd724462c2a6 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Tue, 22 Feb 2022 13:57:43 +0200
Subject: [PATCH 11/38] Resolve realm migration conflicts

---
 .../sdk/internal/database/mapper/ThreadSummaryMapper.kt     | 2 +-
 .../sdk/internal/database/migration/MigrateSessionTo026.kt  | 6 +++---
 .../internal/database/model/threads/ThreadSummaryEntity.kt  | 2 +-
 3 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ThreadSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ThreadSummaryMapper.kt
index 54386c5ea8c..cedb9e3d452 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ThreadSummaryMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ThreadSummaryMapper.kt
@@ -28,7 +28,7 @@ internal class ThreadSummaryMapper @Inject constructor() {
                 roomId = threadSummary.room?.firstOrNull()?.roomId.orEmpty(),
                 rootEvent = threadSummary.rootThreadEventEntity?.asDomain(),
                 latestEvent = threadSummary.latestThreadEventEntity?.asDomain(),
-                rootEventId = threadSummary.rootThreadEventId,
+                rootEventId = threadSummary.rootThreadEventId.orEmpty(),
                 rootThreadSenderInfo = SenderInfo(
                         userId = threadSummary.rootThreadEventEntity?.sender ?: "",
                         displayName = threadSummary.rootThreadSenderName,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt
index 04b64c28936..ac097c916bd 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt
@@ -45,10 +45,10 @@ class MigrateSessionTo026(realm: DynamicRealm) : RealmMigrator(realm, 26) {
                 .addField(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED)
                 .addField(ThreadSummaryEntityFields.ROOT_THREAD_SENDER_NAME, String::class.java)
                 .addField(ThreadSummaryEntityFields.ROOT_THREAD_SENDER_AVATAR, String::class.java)
-                .addField(ThreadSummaryEntityFields.ROOT_THREAD_IS_UNIQUE_DISPLAY_NAME, String::class.java)
+                .addField(ThreadSummaryEntityFields.ROOT_THREAD_IS_UNIQUE_DISPLAY_NAME, Boolean::class.java)
                 .addField(ThreadSummaryEntityFields.LATEST_THREAD_SENDER_NAME, String::class.java)
                 .addField(ThreadSummaryEntityFields.LATEST_THREAD_SENDER_AVATAR, String::class.java)
-                .addField(ThreadSummaryEntityFields.LATEST_THREAD_IS_UNIQUE_DISPLAY_NAME, String::class.java)
+                .addField(ThreadSummaryEntityFields.LATEST_THREAD_IS_UNIQUE_DISPLAY_NAME, Boolean::class.java)
                 .addField(ThreadSummaryEntityFields.NUMBER_OF_THREADS, Int::class.java)
                 .addField(ThreadSummaryEntityFields.IS_USER_PARTICIPATING, Boolean::class.java)
                 .addRealmObjectField(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ENTITY.`$`, eventEntity)
@@ -58,6 +58,6 @@ class MigrateSessionTo026(realm: DynamicRealm) : RealmMigrator(realm, 26) {
                 ?.addRealmListField(RoomEntityFields.THREAD_SUMMARIES.`$`, threadSummaryEntity)
 
         realm.schema.get("HomeServerCapabilitiesEntity")
-                ?.addRealmListField(HomeServerCapabilitiesEntityFields.CAN_USE_THREADING, Boolean::class.java)
+                ?.addField(HomeServerCapabilitiesEntityFields.CAN_USE_THREADING, Boolean::class.java)
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt
index 21a80502e72..ab9d66548ed 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt
@@ -23,7 +23,7 @@ import io.realm.annotations.LinkingObjects
 import org.matrix.android.sdk.internal.database.model.EventEntity
 import org.matrix.android.sdk.internal.database.model.RoomEntity
 
-internal open class ThreadSummaryEntity(@Index var rootThreadEventId: String = "",
+internal open class ThreadSummaryEntity(@Index var rootThreadEventId: String? = "",
                                         var rootThreadEventEntity: EventEntity? = null,
                                         var latestThreadEventEntity: EventEntity? = null,
                                         var rootThreadSenderName: String? = null,

From 2054c577f3ad4e74362ad4ceb8bfe50f1ac04c57 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Tue, 22 Feb 2022 17:41:54 +0200
Subject: [PATCH 12/38] Fix quality check errors

---
 .../sdk/api/session/room/threads/model/ThreadEditions.kt    | 4 ++--
 .../sdk/internal/database/helper/ThreadSummaryHelper.kt     | 6 +++++-
 2 files changed, 7 insertions(+), 3 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadEditions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadEditions.kt
index db92e800e48..05d6fd52cd3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadEditions.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadEditions.kt
@@ -1,11 +1,11 @@
 /*
- * Copyright (c) 2022 New Vector Ltd
+ * Copyright 2020 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
+ * 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,
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 d19056adfa8..2d1b1cccf48 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
@@ -275,7 +275,11 @@ private fun createEventEntity(roomId: String, event: Event, realm: Realm): Event
  * Create an EventEntity for the latest thread event or get an existing one. Also update the user room member
  * state
  */
-private fun createLatestEventEntity(roomId: String, rootThreadEvent: Event, roomMemberContentsByUser: HashMap<String, RoomMemberContent?>, realm: Realm): EventEntity? {
+private fun createLatestEventEntity(
+        roomId: String,
+        rootThreadEvent: Event,
+        roomMemberContentsByUser: HashMap<String, RoomMemberContent?>,
+        realm: Realm): EventEntity? {
     return getLatestEvent(rootThreadEvent)?.let {
         it.senderId?.let { senderId ->
             roomMemberContentsByUser.addSenderState(realm, roomId, senderId)

From f7f363ce257c9cbc66f9cca81b89f974ba860741 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Tue, 22 Feb 2022 20:52:01 +0200
Subject: [PATCH 13/38] Fix wrong copyrights

---
 .../sdk/api/session/room/threads/model/ThreadSummary.kt       | 4 ++--
 .../api/session/room/threads/model/ThreadSummaryUpdateType.kt | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummary.kt
index f26be85e854..017afba1bae 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummary.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummary.kt
@@ -1,11 +1,11 @@
 /*
- * Copyright (c) 2022 New Vector Ltd
+ * Copyright 2022 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
+ * 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,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummaryUpdateType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummaryUpdateType.kt
index 744265cb944..95697f987f0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummaryUpdateType.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummaryUpdateType.kt
@@ -1,11 +1,11 @@
 /*
- * Copyright (c) 2022 New Vector Ltd
+ * Copyright 2022 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
+ * 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,

From 79c97ac5129ea530f56bf8c870bfe10555fa6833 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Tue, 22 Feb 2022 20:59:22 +0200
Subject: [PATCH 14/38] Formating code

---
 .../api/session/room/threads/model/ThreadEditions.kt |  2 +-
 .../internal/database/helper/ThreadSummaryHelper.kt  | 10 +---------
 .../room/EventRelationsAggregationProcessor.kt       | 12 ++++++------
 3 files changed, 8 insertions(+), 16 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadEditions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadEditions.kt
index 05d6fd52cd3..c8353cf0de0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadEditions.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadEditions.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 The Matrix.org Foundation C.I.C.
+ * Copyright 2022 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.
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 2d1b1cccf48..229e433a89a 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
@@ -56,17 +56,9 @@ internal fun ThreadSummaryEntity.updateThreadSummary(
         roomMemberContentsByUser: HashMap<String, RoomMemberContent?>) {
     updateThreadSummaryRootEvent(rootThreadEventEntity, roomMemberContentsByUser)
     updateThreadSummaryLatestEvent(latestThreadEventEntity, roomMemberContentsByUser)
-
-    // Update latest event
-//    latestThreadEventEntity?.toTimelineEventEntity(roomMemberContentsByUser)?.let {
-//        Timber.i("###THREADS FetchThreadSummariesTask ThreadSummaryEntity updated latest event:${it.eventId} !")
-//        this.eventEntity?.threadSummaryLatestMessage = it
-//    }
-
-    // Update number of threads
     this.isUserParticipating = isUserParticipating
     numberOfThreads?.let {
-        // Update only when there is an actual value
+        // Update number of threads only when there is an actual value
         this.numberOfThreads = it
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
index d17d16a82db..573aba1aca0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
@@ -197,15 +197,15 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                             handleReaction(realm, event, roomId, isLocalEcho)
                         }
                     }
-                    // TODO is that ok??
+                    // HandleInitialAggregatedRelations should also be applied in encrypted messages with annotations
 //                    else if (event.unsignedData?.relations?.annotations != null) {
 //                        Timber.v("###REACTION e2e Aggregation in room $roomId for event ${event.eventId}")
 //                        handleInitialAggregatedRelations(realm, event, roomId, event.unsignedData.relations.annotations)
-// //                        EventAnnotationsSummaryEntity.where(realm, roomId, event.eventId ?: "").findFirst()
-// //                                ?.let {
-// //                                    TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId ?: "").findAll()
-// //                                            ?.forEach { tet -> tet.annotations = it }
-// //                                }
+//                         EventAnnotationsSummaryEntity.where(realm, roomId, event.eventId ?: "").findFirst()
+//                                 ?.let {
+//                                     TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId ?: "").findAll()
+//                                             ?.forEach { tet -> tet.annotations = it }
+//                                 }
 //                    }
                 }
                 EventType.REDACTION            -> {

From a9b3882bf6b93cd5a30521eaa01217350b8d62b0 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Wed, 23 Feb 2022 12:06:35 +0200
Subject: [PATCH 15/38] Add changelogs

---
 changelog.d/5230.feature | 1 +
 changelog.d/5232.feature | 1 +
 changelog.d/5271.sdk     | 1 +
 3 files changed, 3 insertions(+)
 create mode 100644 changelog.d/5230.feature
 create mode 100644 changelog.d/5232.feature
 create mode 100644 changelog.d/5271.sdk

diff --git a/changelog.d/5230.feature b/changelog.d/5230.feature
new file mode 100644
index 00000000000..b333a3f2c7a
--- /dev/null
+++ b/changelog.d/5230.feature
@@ -0,0 +1 @@
+Thread timeline is now live and much faster especially for large or old threads
\ No newline at end of file
diff --git a/changelog.d/5232.feature b/changelog.d/5232.feature
new file mode 100644
index 00000000000..8f3bec97bd3
--- /dev/null
+++ b/changelog.d/5232.feature
@@ -0,0 +1 @@
+View all threads per room screen is now live when the home server supports threads
\ No newline at end of file
diff --git a/changelog.d/5271.sdk b/changelog.d/5271.sdk
new file mode 100644
index 00000000000..b73d97ee4f5
--- /dev/null
+++ b/changelog.d/5271.sdk
@@ -0,0 +1 @@
+Adds support for MSC3440, additional threads homeserver capabilities
\ No newline at end of file

From 8788fb974d081f3e67fac56c26eb2427c7562c7f Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Wed, 23 Feb 2022 18:39:02 +0200
Subject: [PATCH 16/38] Add new use case about threads in the allScreensTest

---
 .../vector/app/ui/UiAllScreensSanityTest.kt   | 24 ++++++++++++++
 .../im/vector/app/ui/robot/ElementRobot.kt    | 27 ++++++++++++++--
 .../vector/app/ui/robot/MessageMenuRobot.kt   |  9 ++++++
 .../im/vector/app/ui/robot/RoomDetailRobot.kt | 31 ++++++++++++++++++-
 .../app/ui/robot/settings/SettingsRobot.kt    | 10 ++++--
 .../app/ui/robot/settings/labs/LabFeature.kt  | 26 ++++++++++++++++
 6 files changed, 122 insertions(+), 5 deletions(-)
 create mode 100644 vector/src/androidTest/java/im/vector/app/ui/robot/settings/labs/LabFeature.kt

diff --git a/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt b/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt
index 417d28d6252..5a03d5890a3 100644
--- a/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt
+++ b/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt
@@ -27,6 +27,7 @@ import im.vector.app.espresso.tools.ScreenshotFailureRule
 import im.vector.app.features.MainActivity
 import im.vector.app.getString
 import im.vector.app.ui.robot.ElementRobot
+import im.vector.app.ui.robot.settings.labs.LabFeature
 import im.vector.app.ui.robot.withDeveloperMode
 import org.junit.Rule
 import org.junit.Test
@@ -97,6 +98,8 @@ class UiAllScreensSanityTest {
             }
         }
 
+        testThreadScreens()
+
         elementRobot.space {
             createSpace {
                 crawl()
@@ -148,4 +151,25 @@ class UiAllScreensSanityTest {
         // TODO Deactivate account instead of logout?
         elementRobot.signout(expectSignOutWarning = false)
     }
+
+    /**
+     * Testing multiple threads screens
+     */
+    private fun testThreadScreens() {
+        elementRobot.toggleLabFeature(LabFeature.THREAD_MESSAGES)
+        elementRobot.newRoom {
+            createNewRoom {
+                crawl()
+                createRoom {
+                    val message = "Hello This message will be a thread!"
+                    postMessage(message)
+                    replyToThread(message)
+                    viewInRoom(message)
+                    openThreadSummaries()
+                    selectThreadSummariesFilter()
+                }
+            }
+        }
+        elementRobot.toggleLabFeature(LabFeature.THREAD_MESSAGES)
+    }
 }
diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt
index f0ce23b7db1..3c5de8b2218 100644
--- a/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt
+++ b/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt
@@ -17,9 +17,15 @@
 package im.vector.app.ui.robot
 
 import android.view.View
+import androidx.test.espresso.Espresso.onView
 import androidx.test.espresso.Espresso.pressBack
+import androidx.test.espresso.action.ViewActions
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.assertion.ViewAssertions
 import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
 import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
 import com.adevinta.android.barista.assertion.BaristaVisibilityAssertions.assertDisplayed
 import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
 import com.adevinta.android.barista.interaction.BaristaDialogInteractions.clickDialogNegativeButton
@@ -35,6 +41,7 @@ import im.vector.app.features.home.HomeActivity
 import im.vector.app.features.onboarding.OnboardingActivity
 import im.vector.app.initialSyncIdlingResource
 import im.vector.app.ui.robot.settings.SettingsRobot
+import im.vector.app.ui.robot.settings.labs.LabFeature
 import im.vector.app.ui.robot.space.SpaceRobot
 import im.vector.app.withIdlingResource
 import timber.log.Timber
@@ -70,11 +77,11 @@ class ElementRobot {
         }
     }
 
-    fun settings(block: SettingsRobot.() -> Unit) {
+    fun settings(shouldGoBack: Boolean = true, block: SettingsRobot.() -> Unit) {
         openDrawer()
         clickOn(R.id.homeDrawerHeaderSettingsView)
         block(SettingsRobot())
-        pressBack()
+        if (shouldGoBack) pressBack()
         waitUntilViewVisible(withId(R.id.bottomNavigationView))
     }
 
@@ -103,6 +110,22 @@ class ElementRobot {
         waitUntilViewVisible(withId(R.id.bottomNavigationView))
     }
 
+    fun toggleLabFeature(labFeature: LabFeature) {
+        when (labFeature) {
+            LabFeature.THREAD_MESSAGES -> {
+                settings(shouldGoBack = false) {
+                    labs(shouldGoBack = false) {
+                        onView(withText(R.string.labs_enable_thread_messages))
+                                .check(ViewAssertions.matches(isDisplayed()))
+                                .perform(ViewActions.closeSoftKeyboard(), click())
+                    }
+                }
+            }
+            else                       -> {
+            }
+        }
+    }
+
     fun signout(expectSignOutWarning: Boolean) {
         clickOn(R.id.groupToolbarAvatarImageView)
         clickOn(R.id.homeDrawerHeaderSignoutView)
diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/MessageMenuRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/MessageMenuRobot.kt
index 5973dc34730..5c9ecfdef54 100644
--- a/vector/src/androidTest/java/im/vector/app/ui/robot/MessageMenuRobot.kt
+++ b/vector/src/androidTest/java/im/vector/app/ui/robot/MessageMenuRobot.kt
@@ -70,4 +70,13 @@ class MessageMenuRobot(
         clickOn(R.string.edit)
         autoClosed = true
     }
+
+    fun replyInThread() {
+        clickOn(R.string.reply_in_thread)
+        autoClosed = true
+    }
+    fun viewInRoom() {
+        clickOn(R.string.view_in_room)
+        autoClosed = true
+    }
 }
diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt
index 6cf6ad35517..91409582d96 100644
--- a/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt
+++ b/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt
@@ -62,6 +62,23 @@ class RoomDetailRobot {
         pressBack()
     }
 
+    fun replyToThread(message: String) {
+        openMessageMenu(message) {
+            replyInThread()
+        }
+        val threadMessage = "Hello universe - long message to avoid espresso tapping edited!"
+        writeTo(R.id.composerEditText, threadMessage)
+        waitUntilViewVisible(withId(R.id.sendButton))
+        clickOn(R.id.sendButton)
+    }
+
+    fun viewInRoom(message: String) {
+        openMessageMenu(message) {
+            viewInRoom()
+        }
+        waitUntilViewVisible(withId(R.id.composerEditText))
+    }
+
     fun crawlMessage(message: String) {
         // Test quick reaction
         val quickReaction = EmojiDataSource.quickEmojis[0] // 👍
@@ -110,7 +127,7 @@ class RoomDetailRobot {
         onView(withId(R.id.timelineRecyclerView))
                 .perform(
                         RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
-                                ViewMatchers.hasDescendant(ViewMatchers.withText(message)),
+                                ViewMatchers.hasDescendant(withText(message)),
                                 ViewActions.longClick()
                         )
                 )
@@ -130,4 +147,16 @@ class RoomDetailRobot {
         block(RoomSettingsRobot())
         pressBack()
     }
+
+    fun openThreadSummaries() {
+        clickMenu(R.id.menu_timeline_thread_list)
+        waitUntilViewVisible(withId(R.id.threadListRecyclerView))
+    }
+
+    fun selectThreadSummariesFilter() {
+        clickMenu(R.id.menu_thread_list_filter)
+        sleep(1000)
+        clickOn(R.id.threadListModalMyThreads)
+        pressBack()
+    }
 }
diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsRobot.kt
index 561f14c6f22..97aee7ac4a9 100644
--- a/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsRobot.kt
+++ b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsRobot.kt
@@ -16,6 +16,7 @@
 
 package im.vector.app.ui.robot.settings
 
+import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
 import im.vector.app.R
 import im.vector.app.clickOnAndGoBack
 
@@ -51,8 +52,13 @@ class SettingsRobot {
         clickOnAndGoBack(R.string.settings_security_and_privacy) { block(SettingsSecurityRobot()) }
     }
 
-    fun labs(block: () -> Unit = {}) {
-        clickOnAndGoBack(R.string.room_settings_labs_pref_title) { block() }
+    fun labs(shouldGoBack: Boolean = true, block:  () -> Unit = {}) {
+        if (shouldGoBack) {
+            clickOnAndGoBack(R.string.room_settings_labs_pref_title) { block() }
+        } else {
+            clickOn(R.string.room_settings_labs_pref_title)
+            block()
+        }
     }
 
     fun advancedSettings(block: SettingsAdvancedRobot.() -> Unit) {
diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/settings/labs/LabFeature.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/labs/LabFeature.kt
new file mode 100644
index 00000000000..656201d8128
--- /dev/null
+++ b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/labs/LabFeature.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * 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 im.vector.app.ui.robot.settings.labs
+
+enum class LabFeature {
+    SWIPE_TO_REPLY,
+    TAB_UNREAD_NOTIFICATIONS,
+    LATEX_MATHEMATICS,
+    THREAD_MESSAGES,
+    AUTO_REPORT_ERRORS,
+    RENDER_USER_LOCATION
+}

From eda723c23082382b9f013493b8223195036a2d7d Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Mon, 28 Feb 2022 12:35:27 +0200
Subject: [PATCH 17/38] Remove fetching thread summaries when homeserver do not
 support MSC3440

---
 .../session/homeserver/GetCapabilitiesResult.kt        |  3 ++-
 .../room/threads/list/viewmodel/ThreadListViewModel.kt | 10 ++++++----
 2 files changed, 8 insertions(+), 5 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt
index 55526b41db6..3a016bc3e25 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt
@@ -70,7 +70,8 @@ internal data class Capabilities(
          * Capability to indicate if the server supports MSC3440 Threading
          * True if the user can use m.thread relation, false otherwise
          */
-        @Json(name = "m.thread")
+//        @Json(name = "m.thread")
+        @Json(name = "io.element.thread")
         val threads: BooleanCapability? = null
 )
 
diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt
index 290b71a5041..d68e0a32487 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt
@@ -54,8 +54,7 @@ class ThreadListViewModel @AssistedInject constructor(@Assisted val initialState
     }
 
     init {
-        observeThreads()
-        fetchThreadList()
+        fetchAndObserveThreads()
     }
 
     override fun handle(action: EmptyAction) {}
@@ -64,9 +63,12 @@ class ThreadListViewModel @AssistedInject constructor(@Assisted val initialState
      * Observing thread list with respect to homeserver
      * capabilities
      */
-    private fun observeThreads() {
+    private fun fetchAndObserveThreads() {
         when (session.getHomeServerCapabilities().canUseThreading) {
-            true  -> observeThreadSummaries()
+            true  -> {
+                fetchThreadList()
+                observeThreadSummaries()
+            }
             false -> observeThreadsList()
         }
     }

From 214e0efcd990a6b4c9d491819e010dff9cc13069 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Wed, 2 Mar 2022 13:47:08 +0200
Subject: [PATCH 18/38] Add Markdown support to thread summaries and thread
 list

---
 .../sdk/api/session/threads/ThreadDetails.kt  |   3 +-
 .../internal/database/mapper/EventMapper.kt   |   2 +-
 .../detail/search/SearchResultController.kt   |   3 +
 .../room/detail/search/SearchResultItem.kt    |   4 +-
 .../format/DisplayableEventFormatter.kt       | 100 +++++++++++++++++-
 .../helper/MessageItemAttributesFactory.kt    |   3 +
 .../detail/timeline/item/AbsMessageItem.kt    |   3 +-
 .../list/viewmodel/ThreadListController.kt    |  38 +++++--
 8 files changed, 139 insertions(+), 17 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt
index fafe17b2c09..d6937d5b265 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt
@@ -16,6 +16,7 @@
 
 package org.matrix.android.sdk.api.session.threads
 
+import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.room.sender.SenderInfo
 
 /**
@@ -26,7 +27,7 @@ data class ThreadDetails(
         val isRootThread: Boolean = false,
         val numberOfThreads: Int = 0,
         val threadSummarySenderInfo: SenderInfo? = null,
-        val threadSummaryLatestTextMessage: String? = null,
+        val threadSummaryLatestEvent: Event? = null,
         val lastMessageTimestamp: Long? = null,
         var threadNotificationState: ThreadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE,
         val isThread: Boolean = false,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt
index 9c420e81fd7..c3302f5ccbe 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt
@@ -114,7 +114,7 @@ internal object EventMapper {
                         )
                     },
                     threadNotificationState = eventEntity.threadNotificationState,
-                    threadSummaryLatestTextMessage = eventEntity.threadSummaryLatestMessage?.root?.asDomain()?.getDecryptedTextSummary(),
+                    threadSummaryLatestEvent = eventEntity.threadSummaryLatestMessage?.root?.asDomain(),
                     lastMessageTimestamp = eventEntity.threadSummaryLatestMessage?.root?.originServerTs
 
             )
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt
index 2cdc1a0d908..5b1f17cfe26 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt
@@ -32,6 +32,7 @@ import im.vector.app.core.resources.StringProvider
 import im.vector.app.core.resources.UserPreferencesProvider
 import im.vector.app.core.ui.list.GenericHeaderItem_
 import im.vector.app.features.home.AvatarRenderer
+import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter
 import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
 import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.api.session.events.model.Content
@@ -45,6 +46,7 @@ class SearchResultController @Inject constructor(
         private val avatarRenderer: AvatarRenderer,
         private val stringProvider: StringProvider,
         private val dateFormatter: VectorDateFormatter,
+        private val displayableEventFormatter: DisplayableEventFormatter,
         private val userPreferencesProvider: UserPreferencesProvider
 ) : TypedEpoxyController<SearchViewState>() {
 
@@ -125,6 +127,7 @@ class SearchResultController @Inject constructor(
                     .sender(eventAndSender.sender
                             ?: eventAndSender.event.senderId?.let { session.getRoomMember(it, data.roomId) }?.toMatrixItem())
                     .threadDetails(event.threadDetails)
+                    .threadSummaryFormatted(displayableEventFormatter.formatThreadSummary(event.threadDetails?.threadSummaryLatestEvent).toString())
                     .areThreadMessagesEnabled(userPreferencesProvider.areThreadMessagesEnabled())
                     .listener { listener?.onItemClicked(eventAndSender.event) }
                     .let { result.add(it) }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt
index 2ec786fab2f..3e141ab0e9d 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt
@@ -42,6 +42,7 @@ abstract class SearchResultItem : VectorEpoxyModel<SearchResultItem.Holder>() {
     @EpoxyAttribute lateinit var spannable: EpoxyCharSequence
     @EpoxyAttribute var sender: MatrixItem? = null
     @EpoxyAttribute var threadDetails: ThreadDetails? = null
+    @EpoxyAttribute var threadSummaryFormatted: String? = null
     @EpoxyAttribute var areThreadMessagesEnabled: Boolean = false
 
     @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var listener: ClickListener? = null
@@ -60,8 +61,7 @@ abstract class SearchResultItem : VectorEpoxyModel<SearchResultItem.Holder>() {
                 if (it.isRootThread) {
                     showThreadSummary(holder)
                     holder.threadSummaryCounterTextView.text = it.numberOfThreads.toString()
-                    holder.threadSummaryInfoTextView.text = it.threadSummaryLatestTextMessage.orEmpty()
-
+                    holder.threadSummaryInfoTextView.text = threadSummaryFormatted.orEmpty()
                     val userId = it.threadSummarySenderInfo?.userId ?: return@let
                     val displayName = it.threadSummarySenderInfo?.displayName
                     val avatarUrl = it.threadSummarySenderInfo?.avatarUrl
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt
index d5f3a74e4ea..d4a6f2ee87a 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt
@@ -24,9 +24,11 @@ import im.vector.app.core.resources.StringProvider
 import im.vector.app.features.html.EventHtmlRenderer
 import me.gujun.android.span.span
 import org.commonmark.node.Document
+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.toModel
 import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
+import org.matrix.android.sdk.api.session.room.model.message.MessageContent
 import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageType
@@ -120,14 +122,14 @@ class DisplayableEventFormatter @Inject constructor(
             EventType.CALL_CANDIDATES       -> {
                 span { }
             }
-            EventType.POLL_START                     -> {
+            EventType.POLL_START            -> {
                 timelineEvent.root.getClearContent().toModel<MessagePollContent>(catchError = true)?.pollCreationInfo?.question?.question
                         ?: stringProvider.getString(R.string.sent_a_poll)
             }
-            EventType.POLL_RESPONSE                  -> {
+            EventType.POLL_RESPONSE         -> {
                 stringProvider.getString(R.string.poll_response_room_list_preview)
             }
-            EventType.POLL_END                       -> {
+            EventType.POLL_END              -> {
                 stringProvider.getString(R.string.poll_end_room_list_preview)
             }
             else                            -> {
@@ -139,6 +141,98 @@ class DisplayableEventFormatter @Inject constructor(
         }
     }
 
+    fun formatThreadSummary(
+            event: Event?,
+            latestEdition: String? = null): CharSequence {
+        event ?: return ""
+
+        // There event have been edited
+        if (latestEdition != null) {
+            return run {
+                val localFormattedBody = htmlRenderer.get().parse(latestEdition) as Document
+                val renderedBody = htmlRenderer.get().render(localFormattedBody) ?: latestEdition
+                renderedBody
+            }
+        }
+
+        // The event have been redacted
+        if (event.isRedacted()) {
+            return noticeEventFormatter.formatRedactedEvent(event)
+        }
+
+        // The event is encrypted
+        if (event.isEncrypted() &&
+                event.mxDecryptionResult == null) {
+            return stringProvider.getString(R.string.encrypted_message)
+        }
+
+        return when (event.getClearType()) {
+            EventType.MESSAGE       -> {
+                (event.getClearContent().toModel() as? MessageContent)?.let { messageContent ->
+                    when (messageContent.msgType) {
+                        MessageType.MSGTYPE_TEXT                 -> {
+                            val body = messageContent.getTextDisplayableContent()
+                            if (messageContent is MessageTextContent && messageContent.matrixFormattedBody.isNullOrBlank().not()) {
+                                val localFormattedBody = htmlRenderer.get().parse(body) as Document
+                                val renderedBody = htmlRenderer.get().render(localFormattedBody) ?: body
+                                renderedBody
+                            } else {
+                                body
+                            }
+                        }
+                        MessageType.MSGTYPE_VERIFICATION_REQUEST -> {
+                            stringProvider.getString(R.string.verification_request)
+                        }
+                        MessageType.MSGTYPE_IMAGE                -> {
+                            stringProvider.getString(R.string.sent_an_image)
+                        }
+                        MessageType.MSGTYPE_AUDIO                -> {
+                            if ((messageContent as? MessageAudioContent)?.voiceMessageIndicator != null) {
+                                stringProvider.getString(R.string.sent_a_voice_message)
+                            } else {
+                                stringProvider.getString(R.string.sent_an_audio_file)
+                            }
+                        }
+                        MessageType.MSGTYPE_VIDEO                -> {
+                            stringProvider.getString(R.string.sent_a_video)
+                        }
+                        MessageType.MSGTYPE_FILE                 -> {
+                            stringProvider.getString(R.string.sent_a_file)
+                        }
+                        MessageType.MSGTYPE_LOCATION             -> {
+                            stringProvider.getString(R.string.sent_location)
+                        }
+                        else                                     -> {
+                            messageContent.body
+                        }
+                    }
+                } ?: span { }
+            }
+            EventType.STICKER       -> {
+                stringProvider.getString(R.string.send_a_sticker)
+            }
+            EventType.REACTION      -> {
+                event.getClearContent().toModel<ReactionContent>()?.relatesTo?.let {
+                    emojiSpanify.spanify(stringProvider.getString(R.string.sent_a_reaction, it.key))
+                } ?: span { }
+            }
+            EventType.POLL_START    -> {
+                event.getClearContent().toModel<MessagePollContent>(catchError = true)?.pollCreationInfo?.question?.question
+                        ?: stringProvider.getString(R.string.sent_a_poll)
+            }
+            EventType.POLL_RESPONSE -> {
+                stringProvider.getString(R.string.poll_response_room_list_preview)
+            }
+            EventType.POLL_END      -> {
+                stringProvider.getString(R.string.poll_end_room_list_preview)
+            }
+            else                    -> {
+                span {
+                }
+            }
+        }
+    }
+
     private fun simpleFormat(senderName: String, body: CharSequence, appendAuthor: Boolean): CharSequence {
         return if (appendAuthor) {
             span {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt
index 845b765101f..ef42e32a763 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt
@@ -22,6 +22,7 @@ import im.vector.app.core.resources.UserPreferencesProvider
 import im.vector.app.features.home.AvatarRenderer
 import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
 import im.vector.app.features.home.room.detail.timeline.TimelineEventController
+import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter
 import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
 import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
 import org.matrix.android.sdk.api.session.threads.ThreadDetails
@@ -32,6 +33,7 @@ class MessageItemAttributesFactory @Inject constructor(
         private val messageColorProvider: MessageColorProvider,
         private val avatarSizeProvider: AvatarSizeProvider,
         private val stringProvider: StringProvider,
+        private val displayableEventFormatter: DisplayableEventFormatter,
         private val preferencesProvider: UserPreferencesProvider,
         private val emojiCompatFontProvider: EmojiCompatFontProvider) {
 
@@ -59,6 +61,7 @@ class MessageItemAttributesFactory @Inject constructor(
                 readReceiptsCallback = callback,
                 emojiTypeFace = emojiCompatFontProvider.typeface,
                 decryptionErrorMessage = stringProvider.getString(R.string.encrypted_message),
+                threadSummaryFormatted = displayableEventFormatter.formatThreadSummary(threadDetails?.threadSummaryLatestEvent).toString(),
                 threadDetails = threadDetails,
                 areThreadMessagesEnabled = preferencesProvider.areThreadMessagesEnabled()
         )
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt
index 9e8f86c26ef..bad29bd4445 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt
@@ -115,7 +115,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
             attributes.threadDetails?.let { threadDetails ->
                 holder.threadSummaryConstraintLayout.isVisible = threadDetails.isRootThread
                 holder.threadSummaryCounterTextView.text = threadDetails.numberOfThreads.toString()
-                holder.threadSummaryInfoTextView.text = threadDetails.threadSummaryLatestTextMessage ?: attributes.decryptionErrorMessage
+                holder.threadSummaryInfoTextView.text = attributes.threadSummaryFormatted ?: attributes.decryptionErrorMessage
 
                 val userId = threadDetails.threadSummarySenderInfo?.userId ?: return@let
                 val displayName = threadDetails.threadSummarySenderInfo?.displayName
@@ -183,6 +183,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
             override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
             val emojiTypeFace: Typeface? = null,
             val decryptionErrorMessage: String? = null,
+            val threadSummaryFormatted: String? = null,
             val threadDetails: ThreadDetails? = null,
             val areThreadMessagesEnabled: Boolean = false
     ) : AbsBaseMessageItem.Attributes {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt
index d3a5497d63b..aeef69c6dcf 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt
@@ -17,11 +17,11 @@
 package im.vector.app.features.home.room.threads.list.viewmodel
 
 import com.airbnb.epoxy.EpoxyController
-import im.vector.app.R
 import im.vector.app.core.date.DateFormatKind
 import im.vector.app.core.date.VectorDateFormatter
 import im.vector.app.core.resources.StringProvider
 import im.vector.app.features.home.AvatarRenderer
+import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter
 import im.vector.app.features.home.room.threads.list.model.threadListItem
 import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
@@ -35,6 +35,7 @@ class ThreadListController @Inject constructor(
         private val avatarRenderer: AvatarRenderer,
         private val stringProvider: StringProvider,
         private val dateFormatter: VectorDateFormatter,
+        private val displayableEventFormatter: DisplayableEventFormatter,
         private val session: Session
 ) : EpoxyController() {
 
@@ -70,9 +71,18 @@ class ThreadListController @Inject constructor(
                 }
                 ?.forEach { threadSummary ->
                     val date = dateFormatter.format(threadSummary.latestEvent?.originServerTs, DateFormatKind.ROOM_LIST)
-                    val decryptionErrorMessage = stringProvider.getString(R.string.encrypted_message)
-                    val rootThreadEdition = threadSummary.threadEditions.rootThreadEdition
-                    val latestThreadEdition = threadSummary.threadEditions.latestThreadEdition
+                    val lastMessageFormatted =  threadSummary.let {
+                        displayableEventFormatter.formatThreadSummary(
+                                event = it.latestEvent,
+                                latestEdition = it.threadEditions.latestThreadEdition
+                        ).toString()
+                    }
+                    val rootMessageFormatted =  threadSummary.let {
+                        displayableEventFormatter.formatThreadSummary(
+                                event = it.rootEvent,
+                                latestEdition = it.threadEditions.rootThreadEdition
+                        ).toString()
+                    }
                     threadListItem {
                         id(threadSummary.rootEvent?.eventId)
                         avatarRenderer(host.avatarRenderer)
@@ -82,8 +92,8 @@ class ThreadListController @Inject constructor(
                         rootMessageDeleted(threadSummary.rootEvent?.isRedacted() ?: false)
                         // TODO refactor notifications that with the new thread summary
                         threadNotificationState(threadSummary.rootEvent?.threadDetails?.threadNotificationState ?: ThreadNotificationState.NO_NEW_MESSAGE)
-                        rootMessage(rootThreadEdition ?: threadSummary.rootEvent?.getDecryptedTextSummary() ?: decryptionErrorMessage)
-                        lastMessage(latestThreadEdition ?: threadSummary.latestEvent?.getDecryptedTextSummary() ?: decryptionErrorMessage)
+                        rootMessage(rootMessageFormatted)
+                        lastMessage(lastMessageFormatted)
                         lastMessageCounter(threadSummary.numberOfThreads.toString())
                         lastMessageMatrixItem(threadSummary.latestThreadSenderInfo.toMatrixItemOrNull())
                         itemClickListener {
@@ -112,8 +122,18 @@ class ThreadListController @Inject constructor(
                 }
                 ?.forEach { timelineEvent ->
                     val date = dateFormatter.format(timelineEvent.root.threadDetails?.lastMessageTimestamp, DateFormatKind.ROOM_LIST)
-                    val decryptionErrorMessage = stringProvider.getString(R.string.encrypted_message)
                     val lastRootThreadEdition = timelineEvent.root.threadDetails?.lastRootThreadEdition
+                    val lastMessageFormatted =  timelineEvent.root.threadDetails?.threadSummaryLatestEvent.let {
+                        displayableEventFormatter.formatThreadSummary(
+                                event = it,
+                        ).toString()
+                    }
+                    val rootMessageFormatted =  timelineEvent.root.let {
+                        displayableEventFormatter.formatThreadSummary(
+                                event = it,
+                                latestEdition = lastRootThreadEdition
+                        ).toString()
+                    }
                     threadListItem {
                         id(timelineEvent.eventId)
                         avatarRenderer(host.avatarRenderer)
@@ -122,8 +142,8 @@ class ThreadListController @Inject constructor(
                         date(date)
                         rootMessageDeleted(timelineEvent.root.isRedacted())
                         threadNotificationState(timelineEvent.root.threadDetails?.threadNotificationState ?: ThreadNotificationState.NO_NEW_MESSAGE)
-                        rootMessage(lastRootThreadEdition ?: timelineEvent.root.getDecryptedTextSummary() ?: decryptionErrorMessage)
-                        lastMessage(timelineEvent.root.threadDetails?.threadSummaryLatestTextMessage ?: decryptionErrorMessage)
+                        rootMessage(rootMessageFormatted)
+                        lastMessage(lastMessageFormatted)
                         lastMessageCounter(timelineEvent.root.threadDetails?.numberOfThreads.toString())
                         lastMessageMatrixItem(timelineEvent.root.threadDetails?.threadSummarySenderInfo?.toMatrixItem())
                         itemClickListener {

From 33b170077ea5d342d35a550e4808abdd8f374d18 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Thu, 3 Mar 2022 13:49:53 +0200
Subject: [PATCH 19/38] force refresh home server capabilities

---
 .../sdk/internal/database/migration/MigrateSessionTo026.kt      | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt
index ac097c916bd..f108a91ecf0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt
@@ -23,6 +23,7 @@ import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEnti
 import org.matrix.android.sdk.internal.database.model.RoomEntityFields
 import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
 import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields
+import org.matrix.android.sdk.internal.extensions.forceRefreshOfHomeServerCapabilities
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 
 /**
@@ -59,5 +60,6 @@ class MigrateSessionTo026(realm: DynamicRealm) : RealmMigrator(realm, 26) {
 
         realm.schema.get("HomeServerCapabilitiesEntity")
                 ?.addField(HomeServerCapabilitiesEntityFields.CAN_USE_THREADING, Boolean::class.java)
+                ?.forceRefreshOfHomeServerCapabilities()
     }
 }

From 719e254bb46875965c553c6cc01849c6044fa17a Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Thu, 3 Mar 2022 13:51:41 +0200
Subject: [PATCH 20/38] Format Code

---
 .../sdk/internal/session/room/timeline/LoadTimelineStrategy.kt  | 2 +-
 .../android/sdk/internal/session/room/timeline/TimelineChunk.kt | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
index a26008369a6..a9e7b3bcdc4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
@@ -198,7 +198,7 @@ internal class LoadTimelineStrategy(
             }
         }
         if (mode is Mode.Thread) {
-            return timelineChunk?.loadMoreThread(count, Timeline.Direction.BACKWARDS) ?: LoadMoreResult.FAILURE
+            return timelineChunk?.loadMoreThread(count) ?: LoadMoreResult.FAILURE
         }
         return timelineChunk?.loadMore(count, direction, fetchOnServerIfNeeded) ?: LoadMoreResult.FAILURE
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
index 7ab2e17b9d7..04a5256510a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
@@ -169,7 +169,7 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
      * This function will fetch more live thread timeline events using the /relations api. It will
      * always fetch results, while we want our data to be up to dated.
      */
-    suspend fun loadMoreThread(count: Int, direction: Timeline.Direction): LoadMoreResult {
+    suspend fun loadMoreThread(count: Int, direction: Timeline.Direction = Timeline.Direction.BACKWARDS): LoadMoreResult {
         val rootThreadEventId = timelineSettings.rootThreadEventId ?: return LoadMoreResult.FAILURE
         return if (direction == Timeline.Direction.BACKWARDS) {
             try {

From 39bd437f759c36d2b8c59f5107b7c283c5cc8b31 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Thu, 3 Mar 2022 17:04:08 +0200
Subject: [PATCH 21/38] Temp fix Realm crash

---
 .../session/room/timeline/SendingEventsDataSource.kt       | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt
index b7a2cf2fce3..4c5322d0739 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt
@@ -66,7 +66,12 @@ internal class RealmSendingEventsDataSource(
 
     private fun updateFrozenResults(sendingEvents: RealmList<TimelineEventEntity>?) {
         // Makes sure to close the previous frozen realm
-        frozenSendingTimelineEvents?.realm?.close()
+        // TODO find a better way to avoid thread timeline crash:
+        //  - Make RealmSendingEventsDataSource Singleton
+        //  - Do not initialize RealmSendingEventsDataSource when we are in a thread timeline while
+        //    we already have an instance from the main timeline
+        //  - Close Main timeline before Opening a thread timeline
+        // frozenSendingTimelineEvents?.realm?.close()
         frozenSendingTimelineEvents = sendingEvents?.freeze()
     }
 

From daafddbe7127037bda084327365a035ecd16cdb2 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Thu, 3 Mar 2022 19:10:40 +0200
Subject: [PATCH 22/38] fix Realm crash

---
 .../room/timeline/SendingEventsDataSource.kt  | 25 +++++++++----------
 1 file changed, 12 insertions(+), 13 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt
index 4c5322d0739..1262c09d974 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt
@@ -66,12 +66,9 @@ internal class RealmSendingEventsDataSource(
 
     private fun updateFrozenResults(sendingEvents: RealmList<TimelineEventEntity>?) {
         // Makes sure to close the previous frozen realm
-        // TODO find a better way to avoid thread timeline crash:
-        //  - Make RealmSendingEventsDataSource Singleton
-        //  - Do not initialize RealmSendingEventsDataSource when we are in a thread timeline while
-        //    we already have an instance from the main timeline
-        //  - Close Main timeline before Opening a thread timeline
-        // frozenSendingTimelineEvents?.realm?.close()
+        if (frozenSendingTimelineEvents?.isValid == true) {
+            frozenSendingTimelineEvents?.realm?.close()
+        }
         frozenSendingTimelineEvents = sendingEvents?.freeze()
     }
 
@@ -79,13 +76,15 @@ internal class RealmSendingEventsDataSource(
         val builtSendingEvents = mutableListOf<TimelineEvent>()
         uiEchoManager.getInMemorySendingEvents()
                 .addWithUiEcho(builtSendingEvents)
-        frozenSendingTimelineEvents
-                ?.filter { timelineEvent ->
-                    builtSendingEvents.none { it.eventId == timelineEvent.eventId }
-                }
-                ?.map {
-                    timelineEventMapper.map(it)
-                }?.addWithUiEcho(builtSendingEvents)
+        if (frozenSendingTimelineEvents?.isValid == true) {
+            frozenSendingTimelineEvents
+                    ?.filter { timelineEvent ->
+                        builtSendingEvents.none { it.eventId == timelineEvent.eventId }
+                    }
+                    ?.map {
+                        timelineEventMapper.map(it)
+                    }?.addWithUiEcho(builtSendingEvents)
+        }
 
         return builtSendingEvents
     }

From bce5bc8389dd4a6a09b8b7a50750fc136dcc7e15 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Sat, 5 Mar 2022 17:13:02 +0200
Subject: [PATCH 23/38] Fix wrong versioning regex pattern Add MSC3440 support
 using /version/ and /capabilities

---
 .../api/session/room/threads/ThreadsService.kt   |  2 +-
 .../internal/auth/version/HomeServerVersion.kt   |  3 ++-
 .../sdk/internal/auth/version/Versions.kt        |  9 +++++++++
 .../homeserver/GetHomeServerCapabilitiesTask.kt  |  3 ++-
 .../room/threads/DefaultThreadsService.kt        |  2 +-
 .../android/sdk/api/auth/data/VersionsKtTest.kt  | 16 ++++++++++++++++
 .../list/viewmodel/ThreadListViewModel.kt        |  2 +-
 7 files changed, 32 insertions(+), 5 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt
index 99c0dc7d0fa..839cdff63ba 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt
@@ -42,7 +42,7 @@ interface ThreadsService {
      * message edition for that thread
      * @return the enhanced [List] with edited updates
      */
-    fun enhanceWithEditions(threads: List<ThreadSummary>): List<ThreadSummary>
+    fun enhanceThreadWithEditions(threads: List<ThreadSummary>): List<ThreadSummary>
 
     /**
      * Fetch all thread replies for the specified thread using the /relations api
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt
index 0a9b8b73cc1..815f8de2de2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt
@@ -38,7 +38,7 @@ internal data class HomeServerVersion(
     }
 
     companion object {
-        internal val pattern = Regex("""r(\d+)\.(\d+)\.(\d+)""")
+        internal val pattern = Regex("""[r|v](\d+)\.(\d+)\.(\d+)""")
 
         internal fun parse(value: String): HomeServerVersion? {
             val result = pattern.matchEntire(value) ?: return null
@@ -56,5 +56,6 @@ internal data class HomeServerVersion(
         val r0_4_0 = HomeServerVersion(major = 0, minor = 4, patch = 0)
         val r0_5_0 = HomeServerVersion(major = 0, minor = 5, patch = 0)
         val r0_6_0 = HomeServerVersion(major = 0, minor = 6, patch = 0)
+        val v1_3_0 = HomeServerVersion(major = 1, minor = 3, patch = 0)
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt
index 74cb3de2acf..662348e505a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt
@@ -51,6 +51,7 @@ private const val FEATURE_LAZY_LOAD_MEMBERS = "m.lazy_load_members"
 private const val FEATURE_REQUIRE_IDENTITY_SERVER = "m.require_identity_server"
 private const val FEATURE_ID_ACCESS_TOKEN = "m.id_access_token"
 private const val FEATURE_SEPARATE_ADD_AND_BIND = "m.separate_add_and_bind"
+private const val FEATURE_THREADS_MSC3440 = "org.matrix.msc3440"
 
 /**
  * Return true if the SDK supports this homeserver version
@@ -68,6 +69,14 @@ internal fun Versions.isLoginAndRegistrationSupportedBySdk(): Boolean {
             doesServerSeparatesAddAndBind()
 }
 
+/**
+ * Indicate if the homeserver support MSC3440 for threads
+ */
+internal fun Versions.doesServerSupportThreads(): Boolean {
+    return getMaxVersion() >= HomeServerVersion.v1_3_0 ||
+            unstableFeatures?.get(FEATURE_THREADS_MSC3440) ?: false
+}
+
 /**
  * Return true if the server support the lazy loading of room members
  *
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
index ae8e31c8a16..8be689f49d6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
@@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.extensions.orTrue
 import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
 import org.matrix.android.sdk.internal.auth.version.Versions
+import org.matrix.android.sdk.internal.auth.version.doesServerSupportThreads
 import org.matrix.android.sdk.internal.auth.version.isLoginAndRegistrationSupportedBySdk
 import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity
 import org.matrix.android.sdk.internal.database.query.getOrCreate
@@ -122,7 +123,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
                 homeServerCapabilitiesEntity.roomVersionsJson = capabilities?.roomVersions?.let {
                     MoshiProvider.providesMoshi().adapter(RoomVersions::class.java).toJson(it)
                 }
-                homeServerCapabilitiesEntity.canUseThreading = capabilities?.threads?.enabled.orFalse()
+                homeServerCapabilitiesEntity.canUseThreading = capabilities?.threads?.enabled.orFalse() || getVersionResult?.doesServerSupportThreads().orFalse()
             }
 
             if (getMediaConfigResult != null) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt
index 033c1c0ff9d..b65991347dd 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt
@@ -65,7 +65,7 @@ internal class DefaultThreadsService @AssistedInject constructor(
         )
     }
 
-    override fun enhanceWithEditions(threads: List<ThreadSummary>): List<ThreadSummary> {
+    override fun enhanceThreadWithEditions(threads: List<ThreadSummary>): List<ThreadSummary> {
         return Realm.getInstance(monarchy.realmConfiguration).use {
             threads.enhanceWithEditions(it, roomId)
         }
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt
index 96655b849df..7a38c8a0aa0 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt
@@ -19,6 +19,7 @@ package org.matrix.android.sdk.api.auth.data
 import org.amshove.kluent.shouldBe
 import org.junit.Test
 import org.matrix.android.sdk.internal.auth.version.Versions
+import org.matrix.android.sdk.internal.auth.version.doesServerSupportThreads
 import org.matrix.android.sdk.internal.auth.version.isSupportedBySdk
 
 class VersionsKtTest {
@@ -53,5 +54,20 @@ class VersionsKtTest {
         Versions(supportedVersions = listOf("r0.5.0", "r0.6.0")).isSupportedBySdk() shouldBe true
         Versions(supportedVersions = listOf("r0.5.0", "r0.6.1")).isSupportedBySdk() shouldBe true
         Versions(supportedVersions = listOf("r0.6.0")).isSupportedBySdk() shouldBe true
+        Versions(supportedVersions = listOf("v1.6.0")).isSupportedBySdk() shouldBe true
+    }
+
+    @Test
+    fun doesServerSupportThreads() {
+        Versions(supportedVersions = listOf("r0.6.0")).doesServerSupportThreads() shouldBe false
+        Versions(supportedVersions = listOf("r0.9.1")).doesServerSupportThreads() shouldBe false
+        Versions(supportedVersions = listOf("v1.2.0")).doesServerSupportThreads() shouldBe false
+        Versions(supportedVersions = listOf("v1.3.0")).doesServerSupportThreads() shouldBe true
+        Versions(supportedVersions = listOf("v1.3.1")).doesServerSupportThreads() shouldBe true
+        Versions(supportedVersions = listOf("v1.5.1")).doesServerSupportThreads() shouldBe true
+        Versions(supportedVersions = listOf("r0.6.0"), unstableFeatures = mapOf("org.matrix.msc3440" to true)).doesServerSupportThreads() shouldBe true
+        Versions(supportedVersions = listOf("v1.2.1"), unstableFeatures = mapOf("org.matrix.msc3440" to true)).doesServerSupportThreads() shouldBe true
+        Versions(supportedVersions = listOf("r0.6.0"), unstableFeatures = mapOf("org.matrix.msc3440" to false)).doesServerSupportThreads() shouldBe false
+        Versions(supportedVersions = listOf("v1.4.0"), unstableFeatures = mapOf("org.matrix.msc3440" to false)).doesServerSupportThreads() shouldBe true
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt
index d68e0a32487..7a22f75bce7 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt
@@ -80,7 +80,7 @@ class ThreadListViewModel @AssistedInject constructor(@Assisted val initialState
     private fun observeThreadSummaries() {
         room?.flow()
                 ?.liveThreadSummaries()
-                ?.map { room.enhanceWithEditions(it) }
+                ?.map { room.enhanceThreadWithEditions(it) }
                 ?.flowOn(room.coroutineDispatchers.io)
                 ?.execute { asyncThreads ->
                     copy(threadSummaryList = asyncThreads)

From d19dd91d671f2282b145c7e01152c41cde988387 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Sat, 5 Mar 2022 20:49:11 +0200
Subject: [PATCH 24/38] Format code

---
 .../session/homeserver/GetHomeServerCapabilitiesTask.kt        | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
index 8be689f49d6..1f48082b6b4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
@@ -123,7 +123,8 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
                 homeServerCapabilitiesEntity.roomVersionsJson = capabilities?.roomVersions?.let {
                     MoshiProvider.providesMoshi().adapter(RoomVersions::class.java).toJson(it)
                 }
-                homeServerCapabilitiesEntity.canUseThreading = capabilities?.threads?.enabled.orFalse() || getVersionResult?.doesServerSupportThreads().orFalse()
+                homeServerCapabilitiesEntity.canUseThreading =
+                        capabilities?.threads?.enabled.orFalse() || getVersionResult?.doesServerSupportThreads().orFalse()
             }
 
             if (getMediaConfigResult != null) {

From 557fd7eacf87438a174dae60e6684fd8d2a365f7 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Tue, 8 Mar 2022 10:13:56 +0200
Subject: [PATCH 25/38] Replace thread timeline and thread summaries
 EventInsertType from INCREMENTAL_SYNC to PAGINATION

---
 .../android/sdk/internal/database/helper/ThreadSummaryHelper.kt | 2 +-
 .../session/room/relation/threads/FetchThreadTimelineTask.kt    | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

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 229e433a89a..9ebe1203adb 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
@@ -260,7 +260,7 @@ private fun HashMap<String, RoomMemberContent?>.addSenderState(realm: Realm, roo
  */
 private fun createEventEntity(roomId: String, event: Event, realm: Realm): EventEntity {
     val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
-    return event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC)
+    return event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
 }
 
 /**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
index 54ebc620c9d..a8f712c4fad 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
@@ -212,7 +212,7 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor(
      */
     private fun createEventEntity(roomId: String, event: Event, realm: Realm): EventEntity {
         val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
-        return event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC)
+        return event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
     }
 
     /**

From 8c6902aa23d6532cbd7485e6dbd604097bfce313 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Tue, 8 Mar 2022 14:50:27 +0200
Subject: [PATCH 26/38] Fix reply within thread edition

---
 .../android/sdk/internal/session/filter/RoomEventFilter.kt    | 4 ++--
 .../internal/session/room/relation/DefaultRelationService.kt  | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt
index c93f6a10db2..aac987f3f89 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt
@@ -52,13 +52,13 @@ data class RoomEventFilter(
          * A list of relation types which must be exist pointing to the event being filtered.
          * If this list is absent then no filtering is done on relation types.
          */
-//        @Json(name = "relation_types") val relationTypes: List<String>? = null,
+//        @Json(name = "related_by_rel_types") val relationTypes: List<String>? = null,
         @Json(name = "io.element.relation_types") val relationTypes: List<String>? = null,      // To be replaced with the above line after the release
         /**
          *  A list of senders of relations which must exist pointing to the event being filtered.
          *  If this list is absent then no filtering is done on relation types.
          */
-//        @Json(name = "relation_senders") val relationSenders: List<String>? = null,
+//        @Json(name = "related_by_senders") val relationSenders: List<String>? = null,
         @Json(name = "io.element.relation_senders") val relationSenders: List<String>? = null, // To be replaced with the above line after the release
 
         /**
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 ab514d31c84..bb2acd8438b 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 = false
+                    showInThread = true
             )
                     ?.also {
                         saveLocalEcho(it)

From a53d5bdba2986debbbe7bdeb59fbdb05784b079e Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Tue, 8 Mar 2022 16:41:38 +0200
Subject: [PATCH 27/38] Remove eventType from /relations api for threads

---
 .../android/sdk/internal/session/room/RoomAPI.kt  | 15 ++++++++++++++-
 .../relation/threads/FetchThreadTimelineTask.kt   |  6 +-----
 2 files changed, 15 insertions(+), 6 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
index 71838ab5a29..ceed8466083 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room
 
 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.RelationType
 import org.matrix.android.sdk.api.session.room.model.Membership
 import org.matrix.android.sdk.api.session.room.model.RoomStrippedState
 import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams
@@ -218,7 +219,6 @@ internal interface RoomAPI {
 
     /**
      * Paginate relations for event based in normal topological order
-     *
      * @param relationType filter for this relation type
      * @param eventType filter for this event type
      */
@@ -232,6 +232,19 @@ internal interface RoomAPI {
                              @Query("limit") limit: Int? = null
     ): RelationsResponse
 
+    /**
+     * Paginate relations for thread events based in normal topological order
+     * @param relationType filter for this relation type
+     */
+    @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}")
+    suspend fun getThreadsRelations(@Path("roomId") roomId: String,
+                                    @Path("eventId") eventId: String,
+                                    @Path("relationType") relationType: String = RelationType.IO_THREAD,
+                                    @Query("from") from: String? = null,
+                                    @Query("to") to: String? = null,
+                                    @Query("limit") limit: Int? = null
+    ): RelationsResponse
+
     /**
      * Join the given room.
      *
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
index a8f712c4fad..227558cc75d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
@@ -20,7 +20,6 @@ import io.realm.Realm
 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.EventType
-import org.matrix.android.sdk.api.session.events.model.RelationType
 import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
 import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
@@ -99,14 +98,11 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor(
     }
 
     override suspend fun execute(params: FetchThreadTimelineTask.Params): Result {
-        val isRoomEncrypted = cryptoSessionInfoProvider.isRoomEncrypted(params.roomId)
         val response = executeRequest(globalErrorReceiver) {
-            roomAPI.getRelations(
+            roomAPI.getThreadsRelations(
                     roomId = params.roomId,
                     eventId = params.rootThreadEventId,
-                    relationType = RelationType.IO_THREAD,
                     from = params.from,
-                    eventType = if (isRoomEncrypted) EventType.ENCRYPTED else EventType.MESSAGE,
                     limit = params.limit
             )
         }

From 03f293f2164356186c891ea313ad6134dfbec013 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Thu, 10 Mar 2022 12:06:02 +0200
Subject: [PATCH 28/38] Remove io.element.thread and add stable m.thread prefix

---
 .../api/session/events/model/AggregatedRelations.kt  |  2 +-
 .../android/sdk/api/session/events/model/Event.kt    |  4 ++--
 .../sdk/api/session/events/model/RelationType.kt     |  1 -
 .../sdk/internal/session/filter/FilterFactory.kt     |  2 +-
 .../sdk/internal/session/filter/RoomEventFilter.kt   |  6 ++----
 .../android/sdk/internal/session/room/RoomAPI.kt     |  2 +-
 .../session/room/send/LocalEchoEventFactory.kt       | 12 ++++++------
 .../sdk/internal/session/room/send/TextContent.kt    |  2 +-
 .../sync/handler/room/ThreadsAwarenessHandler.kt     |  4 ++--
 .../features/home/room/detail/TimelineViewModel.kt   |  2 +-
 10 files changed, 17 insertions(+), 20 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt
index 7547d1cfe97..ae8ed3941fa 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt
@@ -50,5 +50,5 @@ import com.squareup.moshi.JsonClass
 data class AggregatedRelations(
         @Json(name = "m.annotation") val annotations: AggregatedAnnotation? = null,
         @Json(name = "m.reference") val references: DefaultUnsignedRelationInfo? = null,
-        @Json(name = RelationType.IO_THREAD) val latestThread: LatestThreadUnsignedRelation? = null
+        @Json(name = RelationType.THREAD) val latestThread: LatestThreadUnsignedRelation? = null
 )
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 97eee9188c5..9d86730d9f5 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
@@ -392,9 +392,9 @@ fun Event.isReplyRenderedInThread(): Boolean {
     return isReply() && getRelationContent()?.inReplyTo?.shouldRenderInThread() == true
 }
 
-fun Event.isThread(): Boolean = getRelationContentForType(RelationType.IO_THREAD)?.eventId != null
+fun Event.isThread(): Boolean = getRelationContentForType(RelationType.THREAD)?.eventId != null
 
-fun Event.getRootThreadEventId(): String? = getRelationContentForType(RelationType.IO_THREAD)?.eventId
+fun Event.getRootThreadEventId(): String? = getRelationContentForType(RelationType.THREAD)?.eventId
 
 fun Event.isEdition(): Boolean {
     return getRelationContentForType(RelationType.REPLACE)?.eventId != null
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 fb26264ad77..74dc74b2949 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
@@ -30,7 +30,6 @@ object RelationType {
 
     /** Lets you define an event which is a thread reply to an existing event.*/
     const val THREAD = "m.thread"
-    const val IO_THREAD = "io.element.thread"
 
     /** 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/filter/FilterFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt
index 2e52354037c..676a4f6a38c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt
@@ -28,7 +28,7 @@ internal object FilterFactory {
                 limit = numberOfEvents,
 //                senders = listOf(userId),
 //                relationSenders = userId?.let { listOf(it) },
-                relationTypes = listOf(RelationType.IO_THREAD)
+                relationTypes = listOf(RelationType.THREAD)
         )
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt
index aac987f3f89..634ea73480a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt
@@ -52,14 +52,12 @@ data class RoomEventFilter(
          * A list of relation types which must be exist pointing to the event being filtered.
          * If this list is absent then no filtering is done on relation types.
          */
-//        @Json(name = "related_by_rel_types") val relationTypes: List<String>? = null,
-        @Json(name = "io.element.relation_types") val relationTypes: List<String>? = null,      // To be replaced with the above line after the release
+        @Json(name = "related_by_rel_types") val relationTypes: List<String>? = null,
         /**
          *  A list of senders of relations which must exist pointing to the event being filtered.
          *  If this list is absent then no filtering is done on relation types.
          */
-//        @Json(name = "related_by_senders") val relationSenders: List<String>? = null,
-        @Json(name = "io.element.relation_senders") val relationSenders: List<String>? = null, // To be replaced with the above line after the release
+        @Json(name = "related_by_senders") val relationSenders: List<String>? = null,
 
         /**
          * A list of room IDs to include. If this list is absent then all rooms are included.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
index ceed8466083..10f75473b71 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
@@ -239,7 +239,7 @@ internal interface RoomAPI {
     @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}")
     suspend fun getThreadsRelations(@Path("roomId") roomId: String,
                                     @Path("eventId") eventId: String,
-                                    @Path("relationType") relationType: String = RelationType.IO_THREAD,
+                                    @Path("relationType") relationType: String = RelationType.THREAD,
                                     @Query("from") from: String? = null,
                                     @Query("to") to: String? = null,
                                     @Query("limit") limit: Int? = null
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 3c36d587107..ef4c09cb9fa 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
@@ -353,7 +353,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                 url = attachment.queryUri.toString(),
                 relatesTo = rootThreadEventId?.let {
                     RelationDefaultContent(
-                            type = RelationType.IO_THREAD,
+                            type = RelationType.THREAD,
                             eventId = it,
                             inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
                     )
@@ -396,7 +396,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                 url = attachment.queryUri.toString(),
                 relatesTo = rootThreadEventId?.let {
                     RelationDefaultContent(
-                            type = RelationType.IO_THREAD,
+                            type = RelationType.THREAD,
                             eventId = it,
                             inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
                     )
@@ -426,7 +426,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                 voiceMessageIndicator = if (!isVoiceMessage) null else emptyMap(),
                 relatesTo = rootThreadEventId?.let {
                     RelationDefaultContent(
-                            type = RelationType.IO_THREAD,
+                            type = RelationType.THREAD,
                             eventId = it,
                             inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
                     )
@@ -446,7 +446,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                 url = attachment.queryUri.toString(),
                 relatesTo = rootThreadEventId?.let {
                     RelationDefaultContent(
-                            type = RelationType.IO_THREAD,
+                            type = RelationType.THREAD,
                             eventId = it,
                             inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
                     )
@@ -479,7 +479,7 @@ internal class LocalEchoEventFactory @Inject constructor(
     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 isThread = (content.toModel<MessageStickerContent>())?.relatesTo?.type == RelationType.THREAD
             val rootThreadEventId = (content.toModel<MessageStickerContent>())?.relatesTo?.eventId
             if (isThread && rootThreadEventId != null) {
                 val newRelationalDefaultContent = (content.toModel<MessageStickerContent>())?.relatesTo?.copy(
@@ -579,7 +579,7 @@ internal class LocalEchoEventFactory @Inject constructor(
     private fun generateReplyRelationContent(eventId: String, rootThreadEventId: String? = null, showAsReply: Boolean): RelationDefaultContent =
             rootThreadEventId?.let {
                 RelationDefaultContent(
-                        type = RelationType.IO_THREAD,
+                        type = RelationType.THREAD,
                         eventId = it,
                         inReplyTo = ReplyToContent(eventId = eventId, renderIn = if (showAsReply) arrayListOf("m.thread") else null))
             } ?: RelationDefaultContent(null, null, ReplyToContent(eventId = eventId))
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 5c629f87f0e..65ae90f2850 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
@@ -58,7 +58,7 @@ fun TextContent.toThreadTextContent(
             format = MessageFormat.FORMAT_MATRIX_HTML.takeIf { formattedText != null },
             body = text,
             relatesTo = RelationDefaultContent(
-                    type = RelationType.IO_THREAD,
+                    type = RelationType.THREAD,
                     eventId = rootThreadEventId,
                     inReplyTo = ReplyToContent(
                             eventId = latestThreadEventId
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 f3a15239553..23db397ccd9 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
@@ -332,7 +332,7 @@ internal class ThreadsAwarenessHandler @Inject constructor(
                 .findAll()
         cacheEventRootId.add(rootThreadEventId)
         return threadList.filter {
-            it.asDomain().getRelationContentForType(RelationType.IO_THREAD)?.inReplyTo?.eventId == currentEventId
+            it.asDomain().getRelationContentForType(RelationType.THREAD)?.inReplyTo?.eventId == currentEventId
         }
     }
 
@@ -350,7 +350,7 @@ internal class ThreadsAwarenessHandler @Inject constructor(
      * @param event
      */
     private fun isThreadEvent(event: Event): Boolean =
-            event.content.toModel<MessageRelationContent>()?.relatesTo?.type == RelationType.IO_THREAD
+            event.content.toModel<MessageRelationContent>()?.relatesTo?.type == RelationType.THREAD
 
     /**
      * Returns the root thread eventId or null otherwise
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
index d5694a5ad83..b93f62eed6d 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
@@ -506,7 +506,7 @@ class TimelineViewModel @AssistedInject constructor(
 
     private fun handleSendSticker(action: RoomDetailAction.SendSticker) {
         val content = initialState.rootThreadEventId?.let {
-            action.stickerContent.copy(relatesTo = RelationDefaultContent(RelationType.IO_THREAD, it))
+            action.stickerContent.copy(relatesTo = RelationDefaultContent(RelationType.THREAD, it))
         } ?: action.stickerContent
 
         room.sendEvent(EventType.STICKER, content.toContent())

From 45ee9f85e5310847006e0ff9785b46957d0d463d Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Thu, 10 Mar 2022 12:07:05 +0200
Subject: [PATCH 29/38] Check if the server supports MSC3440 using the stable
 flag from /versions api

---
 .../org/matrix/android/sdk/internal/auth/version/Versions.kt   | 3 ++-
 .../sdk/internal/session/homeserver/GetCapabilitiesResult.kt   | 3 +--
 .../session/homeserver/GetHomeServerCapabilitiesTask.kt        | 3 +--
 3 files changed, 4 insertions(+), 5 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt
index 662348e505a..d07d5ecd64b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt
@@ -52,6 +52,7 @@ private const val FEATURE_REQUIRE_IDENTITY_SERVER = "m.require_identity_server"
 private const val FEATURE_ID_ACCESS_TOKEN = "m.id_access_token"
 private const val FEATURE_SEPARATE_ADD_AND_BIND = "m.separate_add_and_bind"
 private const val FEATURE_THREADS_MSC3440 = "org.matrix.msc3440"
+private const val FEATURE_THREADS_MSC3440_STABLE = "org.matrix.msc3440.stable"
 
 /**
  * Return true if the SDK supports this homeserver version
@@ -74,7 +75,7 @@ internal fun Versions.isLoginAndRegistrationSupportedBySdk(): Boolean {
  */
 internal fun Versions.doesServerSupportThreads(): Boolean {
     return getMaxVersion() >= HomeServerVersion.v1_3_0 ||
-            unstableFeatures?.get(FEATURE_THREADS_MSC3440) ?: false
+            unstableFeatures?.get(FEATURE_THREADS_MSC3440_STABLE) ?: false
 }
 
 /**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt
index 3a016bc3e25..55526b41db6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt
@@ -70,8 +70,7 @@ internal data class Capabilities(
          * Capability to indicate if the server supports MSC3440 Threading
          * True if the user can use m.thread relation, false otherwise
          */
-//        @Json(name = "m.thread")
-        @Json(name = "io.element.thread")
+        @Json(name = "m.thread")
         val threads: BooleanCapability? = null
 )
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
index 1f48082b6b4..3e4fa4d0095 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
@@ -123,8 +123,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
                 homeServerCapabilitiesEntity.roomVersionsJson = capabilities?.roomVersions?.let {
                     MoshiProvider.providesMoshi().adapter(RoomVersions::class.java).toJson(it)
                 }
-                homeServerCapabilitiesEntity.canUseThreading =
-                        capabilities?.threads?.enabled.orFalse() || getVersionResult?.doesServerSupportThreads().orFalse()
+                homeServerCapabilitiesEntity.canUseThreading = /* capabilities?.threads?.enabled.orFalse() || */ getVersionResult?.doesServerSupportThreads().orFalse()
             }
 
             if (getMediaConfigResult != null) {

From fd30d38603460f334b55b2afbefe148d57d2fd8d Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Thu, 10 Mar 2022 12:51:40 +0200
Subject: [PATCH 30/38] Fix line length

---
 .../session/homeserver/GetHomeServerCapabilitiesTask.kt        | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
index 3e4fa4d0095..44e13d971a8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
@@ -123,7 +123,8 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
                 homeServerCapabilitiesEntity.roomVersionsJson = capabilities?.roomVersions?.let {
                     MoshiProvider.providesMoshi().adapter(RoomVersions::class.java).toJson(it)
                 }
-                homeServerCapabilitiesEntity.canUseThreading = /* capabilities?.threads?.enabled.orFalse() || */ getVersionResult?.doesServerSupportThreads().orFalse()
+                homeServerCapabilitiesEntity.canUseThreading = /* capabilities?.threads?.enabled.orFalse() || */
+                        getVersionResult?.doesServerSupportThreads().orFalse()
             }
 
             if (getMediaConfigResult != null) {

From a758ad71e665361f707f8b74c19274a904defd1e Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Thu, 10 Mar 2022 17:51:02 +0200
Subject: [PATCH 31/38] 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 9d86730d9f5..817d666cf85 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 733d6c37e86..e7bebeeff66 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 e2080bb4376..f5b3f4c75e9 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 10b071a6013..5dcb1b4323b 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 412a1bfca9d..251328bea21 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<String>? = 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 9ebe1203adb..7087f071621 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 bb2acd8438b..ab514d31c84 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 b54cd71e508..62a910f79da 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 ef4c09cb9fa..d53c375cbf2 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 65ae90f2850..93c0167abe8 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 02855e7ea24..8fe85f0d318 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<String, RoomSync>) : 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<Event>,
-                                     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<Event>,
+                                             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 23db397ccd9..dc0cc52a728 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, Any>): String? {
+                                        eventPayload: MutableMap<String, Any>,
+                                        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<MessageRelationContent>()?.relatesTo?.eventId
 
+    private fun getRootThreadRelationContent(event: Event): RelationDefaultContent? =
+            event.content.toModel<MessageRelationContent>()?.relatesTo
+
     private fun getPreviousEventOrRoot(event: Event): String? =
             event.content.toModel<MessageRelationContent>()?.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 325e9b93302..f7975c90293 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(

From f31b130b494e5890c2b00248ef789ccb90c448f0 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Thu, 10 Mar 2022 19:11:14 +0200
Subject: [PATCH 32/38] Fix unit tests

---
 .../matrix/android/sdk/api/auth/data/VersionsKtTest.kt    | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt
index 7a38c8a0aa0..088e1609503 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt
@@ -65,9 +65,9 @@ class VersionsKtTest {
         Versions(supportedVersions = listOf("v1.3.0")).doesServerSupportThreads() shouldBe true
         Versions(supportedVersions = listOf("v1.3.1")).doesServerSupportThreads() shouldBe true
         Versions(supportedVersions = listOf("v1.5.1")).doesServerSupportThreads() shouldBe true
-        Versions(supportedVersions = listOf("r0.6.0"), unstableFeatures = mapOf("org.matrix.msc3440" to true)).doesServerSupportThreads() shouldBe true
-        Versions(supportedVersions = listOf("v1.2.1"), unstableFeatures = mapOf("org.matrix.msc3440" to true)).doesServerSupportThreads() shouldBe true
-        Versions(supportedVersions = listOf("r0.6.0"), unstableFeatures = mapOf("org.matrix.msc3440" to false)).doesServerSupportThreads() shouldBe false
-        Versions(supportedVersions = listOf("v1.4.0"), unstableFeatures = mapOf("org.matrix.msc3440" to false)).doesServerSupportThreads() shouldBe true
+        Versions(supportedVersions = listOf("r0.6.0"), unstableFeatures = mapOf("org.matrix.msc3440.stable" to true)).doesServerSupportThreads() shouldBe true
+        Versions(supportedVersions = listOf("v1.2.1"), unstableFeatures = mapOf("org.matrix.msc3440.stable" to true)).doesServerSupportThreads() shouldBe true
+        Versions(supportedVersions = listOf("r0.6.0"), unstableFeatures = mapOf("org.matrix.msc3440.stable" to false)).doesServerSupportThreads() shouldBe false
+        Versions(supportedVersions = listOf("v1.4.0"), unstableFeatures = mapOf("org.matrix.msc3440.stable" to false)).doesServerSupportThreads() shouldBe true
     }
 }

From c2ec7cfa0f7371b4487c79ec6e2f9e90760ef669 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Mon, 14 Mar 2022 11:54:29 +0100
Subject: [PATCH 33/38] Add more clear documentation

---
 .../sdk/api/session/room/model/relation/RelationContent.kt  | 6 +++++-
 .../sdk/internal/session/room/relation/EventEditor.kt       | 2 +-
 2 files changed, 6 insertions(+), 2 deletions(-)

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 f5b3f4c75e9..8eba59755eb 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,5 +24,9 @@ interface RelationContent {
     val eventId: String?
     val inReplyTo: ReplyToContent?
     val option: Int?
-    val isFallingBack: Boolean? // Thread fallback to differentiate replies within threads
+    /**
+     * This flag indicates that the message should be displayed in the main
+     * timeline as a reply if needed
+     */
+    val isFallingBack: Boolean?
 }
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 62a910f79da..b54cd71e508 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    // Test that value
+                    showInThread = false
             )?.copy(
                     eventId = replyToEdit.eventId
             ) ?: return NoOpCancellable

From fef36d93349b811ed99367314121c422cb659057 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Mon, 14 Mar 2022 12:25:11 +0100
Subject: [PATCH 34/38] Temporarily fix develop crash

---
 vector/src/main/res/layout/item_search_result.xml       | 6 ++++++
 vector/src/main/res/layout/item_timeline_event_base.xml | 6 +++++-
 2 files changed, 11 insertions(+), 1 deletion(-)

diff --git a/vector/src/main/res/layout/item_search_result.xml b/vector/src/main/res/layout/item_search_result.xml
index 3264a7d2308..3f5ef0c9739 100644
--- a/vector/src/main/res/layout/item_search_result.xml
+++ b/vector/src/main/res/layout/item_search_result.xml
@@ -36,6 +36,12 @@
         app:layout_constraintTop_toTopOf="parent"
         tools:text="@sample/users.json/data/displayName" />
 
+    <View
+        android:id="@+id/additionalTopSpace"
+        android:layout_height="12dp"
+        android:layout_width="0dp"
+        android:layout_toEndOf="@id/messageStartGuideline" />
+
     <TextView
         android:id="@+id/messageTimeView"
         style="@style/Widget.Vector.TextView.Caption"
diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml
index bc02728f6e0..7ee03743652 100644
--- a/vector/src/main/res/layout/item_timeline_event_base.xml
+++ b/vector/src/main/res/layout/item_timeline_event_base.xml
@@ -24,7 +24,11 @@
         android:layout_marginTop="4dp"
         android:contentDescription="@string/avatar"
         tools:src="@sample/user_round_avatars" />
-
+    <View
+        android:id="@+id/additionalTopSpace"
+        android:layout_height="12dp"
+        android:layout_width="0dp"
+        android:layout_toEndOf="@id/messageStartGuideline" />
     <TextView
         android:id="@+id/messageMemberNameView"
         style="@style/Widget.Vector.TextView.Subtitle"

From d894d8598c19a84f196e4603d6f102687c2d61f7 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Mon, 14 Mar 2022 12:44:25 +0100
Subject: [PATCH 35/38] Format text

---
 .../sdk/api/session/room/model/relation/RelationContent.kt       | 1 +
 1 file changed, 1 insertion(+)

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 8eba59755eb..3ba453b7c4e 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,6 +24,7 @@ interface RelationContent {
     val eventId: String?
     val inReplyTo: ReplyToContent?
     val option: Int?
+
     /**
      * This flag indicates that the message should be displayed in the main
      * timeline as a reply if needed

From d7c486c55e4312295208bbd4b5371b3b0a0299ac Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Mon, 14 Mar 2022 16:04:08 +0100
Subject: [PATCH 36/38] Add fallback support rendering proposed in MSC3676

---
 .../session/room/model/relation/RelationContent.kt   |  4 ++--
 .../session/room/send/LocalEchoEventFactory.kt       |  4 ++++
 .../sync/handler/room/ThreadsAwarenessHandler.kt     | 12 +++++++++++-
 .../features/home/room/detail/TimelineViewModel.kt   |  5 ++++-
 vector/src/main/res/layout/item_search_result.xml    |  3 ++-
 5 files changed, 23 insertions(+), 5 deletions(-)

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 3ba453b7c4e..53b1fea873d 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
@@ -26,8 +26,8 @@ interface RelationContent {
     val option: Int?
 
     /**
-     * This flag indicates that the message should be displayed in the main
-     * timeline as a reply if needed
+     * This flag indicates that the message should be rendered as a reply
+     * fallback, when isFallingBack = false
      */
     val isFallingBack: Boolean?
 }
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 d53c375cbf2..3c23cd1869e 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
@@ -355,6 +355,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                     RelationDefaultContent(
                             type = RelationType.THREAD,
                             eventId = it,
+                            isFallingBack = true,
                             inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
                     )
                 }
@@ -398,6 +399,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                     RelationDefaultContent(
                             type = RelationType.THREAD,
                             eventId = it,
+                            isFallingBack = true,
                             inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
                     )
                 }
@@ -428,6 +430,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                     RelationDefaultContent(
                             type = RelationType.THREAD,
                             eventId = it,
+                            isFallingBack = true,
                             inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
                     )
                 }
@@ -448,6 +451,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                     RelationDefaultContent(
                             type = RelationType.THREAD,
                             eventId = it,
+                            isFallingBack = true,
                             inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
                     )
                 }
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 dc0cc52a728..db9799d51eb 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
@@ -162,7 +162,7 @@ internal class ThreadsAwarenessHandler @Inject constructor(
                              eventEntity: EventEntity? = null): String? {
         event ?: return null
         roomId ?: return null
-        if (lightweightSettingsStorage.areThreadMessagesEnabled()) return null
+        if (lightweightSettingsStorage.areThreadMessagesEnabled() && !isReplyEvent(event)) return null
         handleRootThreadEventsIfNeeded(realm, roomId, eventEntity, event)
         if (!isThreadEvent(event)) return null
         val eventPayload = if (!event.isEncrypted()) {
@@ -387,6 +387,16 @@ internal class ThreadsAwarenessHandler @Inject constructor(
     private fun getPreviousEventOrRoot(event: Event): String? =
             event.content.toModel<MessageRelationContent>()?.relatesTo?.inReplyTo?.eventId
 
+    /**
+     * Returns if we should html inject the current event.
+     */
+    private fun isReplyEvent(event: Event): Boolean {
+        return isThreadEvent(event) && !isFallingBack(event) && getPreviousEventOrRoot(event) != null
+    }
+
+    private fun isFallingBack(event: Event): Boolean =
+            event.content.toModel<MessageRelationContent>()?.relatesTo?.isFallingBack == true
+
     @Suppress("UNCHECKED_CAST")
     private fun getValueFromPayload(payload: JsonDict?, key: String): String? {
         val content = payload?.get("content") as? JsonDict
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
index b93f62eed6d..a9235b56994 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
@@ -506,7 +506,10 @@ class TimelineViewModel @AssistedInject constructor(
 
     private fun handleSendSticker(action: RoomDetailAction.SendSticker) {
         val content = initialState.rootThreadEventId?.let {
-            action.stickerContent.copy(relatesTo = RelationDefaultContent(RelationType.THREAD, it))
+            action.stickerContent.copy(relatesTo = RelationDefaultContent(
+                    type = RelationType.THREAD,
+                    isFallingBack = true,
+                    eventId = it))
         } ?: action.stickerContent
 
         room.sendEvent(EventType.STICKER, content.toContent())
diff --git a/vector/src/main/res/layout/item_search_result.xml b/vector/src/main/res/layout/item_search_result.xml
index 3f5ef0c9739..0376ad11106 100644
--- a/vector/src/main/res/layout/item_search_result.xml
+++ b/vector/src/main/res/layout/item_search_result.xml
@@ -40,7 +40,8 @@
         android:id="@+id/additionalTopSpace"
         android:layout_height="12dp"
         android:layout_width="0dp"
-        android:layout_toEndOf="@id/messageStartGuideline" />
+        android:layout_toEndOf="@id/messageStartGuideline"
+        tools:ignore="MissingConstraints" />
 
     <TextView
         android:id="@+id/messageTimeView"

From c9f07061ef0614bdd3ee7b2a70c9c40502a7c78a Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Tue, 15 Mar 2022 11:26:00 +0100
Subject: [PATCH 37/38] Revert temporary fix for develop crash

---
 vector/src/main/res/layout/item_search_result.xml       | 7 -------
 vector/src/main/res/layout/item_timeline_event_base.xml | 6 +-----
 2 files changed, 1 insertion(+), 12 deletions(-)

diff --git a/vector/src/main/res/layout/item_search_result.xml b/vector/src/main/res/layout/item_search_result.xml
index 0376ad11106..3264a7d2308 100644
--- a/vector/src/main/res/layout/item_search_result.xml
+++ b/vector/src/main/res/layout/item_search_result.xml
@@ -36,13 +36,6 @@
         app:layout_constraintTop_toTopOf="parent"
         tools:text="@sample/users.json/data/displayName" />
 
-    <View
-        android:id="@+id/additionalTopSpace"
-        android:layout_height="12dp"
-        android:layout_width="0dp"
-        android:layout_toEndOf="@id/messageStartGuideline"
-        tools:ignore="MissingConstraints" />
-
     <TextView
         android:id="@+id/messageTimeView"
         style="@style/Widget.Vector.TextView.Caption"
diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml
index 7ee03743652..bc02728f6e0 100644
--- a/vector/src/main/res/layout/item_timeline_event_base.xml
+++ b/vector/src/main/res/layout/item_timeline_event_base.xml
@@ -24,11 +24,7 @@
         android:layout_marginTop="4dp"
         android:contentDescription="@string/avatar"
         tools:src="@sample/user_round_avatars" />
-    <View
-        android:id="@+id/additionalTopSpace"
-        android:layout_height="12dp"
-        android:layout_width="0dp"
-        android:layout_toEndOf="@id/messageStartGuideline" />
+
     <TextView
         android:id="@+id/messageMemberNameView"
         style="@style/Widget.Vector.TextView.Subtitle"

From 4d76c0d822c48266a38b570fe5399cbfdd8de301 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Tue, 15 Mar 2022 14:34:53 +0100
Subject: [PATCH 38/38] Fix build error

---
 .../detail/timeline/format/DisplayableEventFormatter.kt     | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt
index 29e12de228b..b83322dc9b7 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt
@@ -216,14 +216,14 @@ class DisplayableEventFormatter @Inject constructor(
                     emojiSpanify.spanify(stringProvider.getString(R.string.sent_a_reaction, it.key))
                 } ?: span { }
             }
-            EventType.POLL_START    -> {
+            in EventType.POLL_START    -> {
                 event.getClearContent().toModel<MessagePollContent>(catchError = true)?.pollCreationInfo?.question?.question
                         ?: stringProvider.getString(R.string.sent_a_poll)
             }
-            EventType.POLL_RESPONSE -> {
+            in EventType.POLL_RESPONSE -> {
                 stringProvider.getString(R.string.poll_response_room_list_preview)
             }
-            EventType.POLL_END      -> {
+            in EventType.POLL_END      -> {
                 stringProvider.getString(R.string.poll_end_room_list_preview)
             }
             else                    -> {