diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
index ca65b3ec5c8debdf74ba65d4b1173a111821f247..c6930dba26e38c3f9008d04f04d1210b5be1198a 100644
--- a/.idea/jarRepositories.xml
+++ b/.idea/jarRepositories.xml
@@ -31,5 +31,10 @@
       <option name="name" value="maven2" />
       <option name="url" value="https://github.com/vector-im/jitsi_libre_maven/raw/master/android-sdk-2.9.3" />
     </remote-repository>
+    <remote-repository>
+      <option name="id" value="MavenRepo" />
+      <option name="name" value="MavenRepo" />
+      <option name="url" value="https://repo.maven.apache.org/maven2/" />
+    </remote-repository>
   </component>
 </project>
\ No newline at end of file
diff --git a/CHANGES.md b/CHANGES.md
index cc0f3ac3a89e948abb9ae6abafc9e47aaacd69c3..6a11606c058b7439815bf94722fcdf07b87cee64 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,15 @@
 Please also refer to the Changelog of Element Android: https://github.com/vector-im/element-android/blob/main/CHANGES.md
 
+Changes in Matrix-SDK 1.4.2 (2022-02-28)
+===================================================
+
+SDK API changes ⚠️
+------------------
+ - `join` and `leave` methods moved from MembershipService to RoomService and SpaceService to split logic for rooms and spaces ([#5183](https://github.com/vector-im/element-android/issues/5183))
+ - Deprecates Matrix.initialize and Matrix.getInstance in favour of the client providing its own singleton instance via Matrix.createInstance ([#5185](https://github.com/vector-im/element-android/issues/5185))
+ - Adds support for MSC3283, additional homeserver capabilities ([#5207](https://github.com/vector-im/element-android/issues/5207))
+
+
 Changes in Matrix-SDK 1.3.18 (2022-02-04)
 ===================================================
 
diff --git a/gradle.properties b/gradle.properties
index cb10f71f955feb13c9f6f98c460ee4bf2cb59860..fe7693929ea82af1313670b391c5de56f98fbbce 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -26,7 +26,7 @@ vector.httpLogLevel=NONE
 # Ref: https://github.com/vanniktech/gradle-maven-publish-plugin
 GROUP=org.matrix.android
 POM_ARTIFACT_ID=matrix-android-sdk2
-VERSION_NAME=1.3.18
+VERSION_NAME=1.4.2
 
 POM_PACKAGING=aar
 
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index ee6ba9a3ac42da2ff9199cd5c96c2cb98c24d978..dcf5e2cb7b3ee7f877575809b96a7da81ca76e26 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionSha256Sum=c9490e938b221daf0094982288e4038deed954a3f12fb54cbf270ddf4e37d879
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip
+distributionSha256Sum=cd5c2958a107ee7f0722004a12d0f8559b4564c34daad7df06cffd4d12a426d0
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle
index 60a1a49249634aa62ceb2a9a461c80a4753a83e0..c7a8d9f1960761a249819f4ec5f8aded956a1d4f 100644
--- a/matrix-sdk-android/build.gradle
+++ b/matrix-sdk-android/build.gradle
@@ -142,6 +142,9 @@ dependencies {
 
     kapt 'dk.ilios:realmfieldnameshelper:2.0.0'
 
+    // Shared Preferences
+    implementation libs.androidx.preferenceKtx
+
     // Work
     implementation libs.androidx.work
 
@@ -167,7 +170,7 @@ dependencies {
     implementation libs.apache.commonsImaging
 
     // Phone number https://github.com/google/libphonenumber
-    implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.42'
+    implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.43'
 
     testImplementation libs.tests.junit
     testImplementation 'org.robolectric:robolectric:4.7.3'
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
index 3cb699378fa2599cc7969e0e39f3043963a92102..031d0a8bcf8d1fab82eec03ef840fe43449c642b 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
@@ -157,14 +157,20 @@ class CommonTestHelper(context: Context) {
     /**
      * Will send nb of messages provided by count parameter but waits every 10 messages to avoid gap in sync
      */
-    private fun sendTextMessagesBatched(timeline: Timeline, room: Room, message: String, count: Int, timeout: Long): List<TimelineEvent> {
+    private fun sendTextMessagesBatched(timeline: Timeline, room: Room, message: String, count: Int, timeout: Long, rootThreadEventId: String? = null): List<TimelineEvent> {
         val sentEvents = ArrayList<TimelineEvent>(count)
         (1 until count + 1)
                 .map { "$message #$it" }
                 .chunked(10)
                 .forEach { batchedMessages ->
                     batchedMessages.forEach { formattedMessage ->
-                        room.sendTextMessage(formattedMessage)
+                        if (rootThreadEventId != null) {
+                            room.replyInThread(
+                                    rootThreadEventId = rootThreadEventId,
+                                    replyInThreadText = formattedMessage)
+                        } else {
+                            room.sendTextMessage(formattedMessage)
+                        }
                     }
                     waitWithLatch(timeout) { latch ->
                         val timelineListener = object : Timeline.Listener {
@@ -196,6 +202,27 @@ class CommonTestHelper(context: Context) {
         return sentEvents
     }
 
+    /**
+     * Reply in a thread
+     * @param room         the room where to send the messages
+     * @param message      the message to send
+     * @param numberOfMessages the number of time the message will be sent
+     */
+    fun replyInThreadMessage(
+            room: Room,
+            message: String,
+            numberOfMessages: Int,
+            rootThreadEventId: String,
+            timeout: Long = TestConstants.timeOutMillis): List<TimelineEvent> {
+        val timeline = room.createTimeline(null, TimelineSettings(10))
+        timeline.start()
+        val sentEvents = sendTextMessagesBatched(timeline, room, message, numberOfMessages, timeout, rootThreadEventId)
+        timeline.dispose()
+        // Check that all events has been created
+        assertEquals("Message number do not match $sentEvents", numberOfMessages.toLong(), sentEvents.size.toLong())
+        return sentEvents
+    }
+
     // PRIVATE METHODS *****************************************************************************
 
     /**
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6aa4f4cc32b3284427d692ed029e8827eba1b022
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt
@@ -0,0 +1,339 @@
+/*
+ * 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.session.room.threads
+
+import org.amshove.kluent.shouldBe
+import org.amshove.kluent.shouldBeEqualTo
+import org.amshove.kluent.shouldBeFalse
+import org.amshove.kluent.shouldBeNull
+import org.amshove.kluent.shouldBeTrue
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.junit.runners.MethodSorters
+import org.matrix.android.sdk.InstrumentedTest
+import org.matrix.android.sdk.api.session.events.model.getRootThreadEventId
+import org.matrix.android.sdk.api.session.events.model.isTextMessage
+import org.matrix.android.sdk.api.session.events.model.isThread
+import org.matrix.android.sdk.api.session.room.timeline.Timeline
+import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
+import org.matrix.android.sdk.common.CommonTestHelper
+import org.matrix.android.sdk.common.CryptoTestHelper
+import java.util.concurrent.CountDownLatch
+
+@RunWith(JUnit4::class)
+@FixMethodOrder(MethodSorters.JVM)
+class ThreadMessagingTest : InstrumentedTest {
+
+    @Test
+    fun reply_in_thread_should_create_a_thread() {
+        val commonTestHelper = CommonTestHelper(context())
+        val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
+        val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false)
+
+        val aliceSession = cryptoTestData.firstSession
+        val aliceRoomId = cryptoTestData.roomId
+
+        val aliceRoom = aliceSession.getRoom(aliceRoomId)!!
+
+        // Let's send a message in the normal timeline
+        val textMessage = "This is a normal timeline message"
+        val sentMessages = commonTestHelper.sendTextMessage(
+                room = aliceRoom,
+                message = textMessage,
+                nbOfMessages = 1)
+
+        val initMessage = sentMessages.first()
+
+        initMessage.root.isThread().shouldBeFalse()
+        initMessage.root.isTextMessage().shouldBeTrue()
+        initMessage.root.getRootThreadEventId().shouldBeNull()
+        initMessage.root.threadDetails?.isRootThread?.shouldBeFalse()
+
+        // Let's reply in timeline to that message
+        val repliesInThread = commonTestHelper.replyInThreadMessage(
+                room = aliceRoom,
+                message = "Reply In the above thread",
+                numberOfMessages = 1,
+                rootThreadEventId = initMessage.root.eventId.orEmpty())
+
+        val replyInThread = repliesInThread.first()
+        replyInThread.root.isThread().shouldBeTrue()
+        replyInThread.root.isTextMessage().shouldBeTrue()
+        replyInThread.root.getRootThreadEventId().shouldBeEqualTo(initMessage.root.eventId)
+
+        // The init normal message should now be a root thread event
+        val timeline = aliceRoom.createTimeline(null, TimelineSettings(30))
+        timeline.start()
+
+        aliceSession.startSync(true)
+        run {
+            val lock = CountDownLatch(1)
+            val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
+                val initMessageThreadDetails = snapshot.firstOrNull {
+                    it.root.eventId == initMessage.root.eventId
+                }?.root?.threadDetails
+                initMessageThreadDetails?.isRootThread?.shouldBeTrue()
+                initMessageThreadDetails?.numberOfThreads?.shouldBe(1)
+                true
+            }
+            timeline.addListener(eventsListener)
+            commonTestHelper.await(lock, 600_000)
+        }
+        aliceSession.stopSync()
+    }
+
+    @Test
+    fun reply_in_thread_should_create_a_thread_from_other_user() {
+        val commonTestHelper = CommonTestHelper(context())
+        val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
+        val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
+
+        val aliceSession = cryptoTestData.firstSession
+        val aliceRoomId = cryptoTestData.roomId
+        val aliceRoom = aliceSession.getRoom(aliceRoomId)!!
+
+        // Let's send a message in the normal timeline
+        val textMessage = "This is a normal timeline message"
+        val sentMessages = commonTestHelper.sendTextMessage(
+                room = aliceRoom,
+                message = textMessage,
+                nbOfMessages = 1)
+
+        val initMessage = sentMessages.first()
+
+        initMessage.root.isThread().shouldBeFalse()
+        initMessage.root.isTextMessage().shouldBeTrue()
+        initMessage.root.getRootThreadEventId().shouldBeNull()
+        initMessage.root.threadDetails?.isRootThread?.shouldBeFalse()
+
+        // Let's reply in timeline to that message from another user
+        val bobSession = cryptoTestData.secondSession!!
+        val bobRoomId = cryptoTestData.roomId
+        val bobRoom = bobSession.getRoom(bobRoomId)!!
+
+        val repliesInThread = commonTestHelper.replyInThreadMessage(
+                room = bobRoom,
+                message = "Reply In the above thread",
+                numberOfMessages = 1,
+                rootThreadEventId = initMessage.root.eventId.orEmpty())
+
+        val replyInThread = repliesInThread.first()
+        replyInThread.root.isThread().shouldBeTrue()
+        replyInThread.root.isTextMessage().shouldBeTrue()
+        replyInThread.root.getRootThreadEventId().shouldBeEqualTo(initMessage.root.eventId)
+
+        // The init normal message should now be a root thread event
+        val timeline = aliceRoom.createTimeline(null, TimelineSettings(30))
+        timeline.start()
+
+        aliceSession.startSync(true)
+        run {
+            val lock = CountDownLatch(1)
+            val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
+                val initMessageThreadDetails = snapshot.firstOrNull { it.root.eventId == initMessage.root.eventId }?.root?.threadDetails
+                initMessageThreadDetails?.isRootThread?.shouldBeTrue()
+                initMessageThreadDetails?.numberOfThreads?.shouldBe(1)
+                true
+            }
+            timeline.addListener(eventsListener)
+            commonTestHelper.await(lock, 600_000)
+        }
+        aliceSession.stopSync()
+
+        bobSession.startSync(true)
+        run {
+            val lock = CountDownLatch(1)
+            val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
+                val initMessageThreadDetails = snapshot.firstOrNull { it.root.eventId == initMessage.root.eventId }?.root?.threadDetails
+                initMessageThreadDetails?.isRootThread?.shouldBeTrue()
+                initMessageThreadDetails?.numberOfThreads?.shouldBe(1)
+                true
+            }
+            timeline.addListener(eventsListener)
+            commonTestHelper.await(lock, 600_000)
+        }
+        bobSession.stopSync()
+    }
+
+    @Test
+    fun reply_in_thread_to_timeline_message_multiple_times() {
+        val commonTestHelper = CommonTestHelper(context())
+        val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
+        val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false)
+
+        val aliceSession = cryptoTestData.firstSession
+        val aliceRoomId = cryptoTestData.roomId
+
+        val aliceRoom = aliceSession.getRoom(aliceRoomId)!!
+
+        // Let's send 5 messages in the normal timeline
+        val textMessage = "This is a normal timeline message"
+        val sentMessages = commonTestHelper.sendTextMessage(
+                room = aliceRoom,
+                message = textMessage,
+                nbOfMessages = 5)
+
+        sentMessages.forEach {
+            it.root.isThread().shouldBeFalse()
+            it.root.isTextMessage().shouldBeTrue()
+            it.root.getRootThreadEventId().shouldBeNull()
+            it.root.threadDetails?.isRootThread?.shouldBeFalse()
+        }
+        // let's start the thread from the second message
+        val selectedInitMessage = sentMessages[1]
+
+        // Let's reply 40 times in the timeline to the second message
+        val repliesInThread = commonTestHelper.replyInThreadMessage(
+                room = aliceRoom,
+                message = "Reply In the above thread",
+                numberOfMessages = 40,
+                rootThreadEventId = selectedInitMessage.root.eventId.orEmpty())
+
+        repliesInThread.forEach {
+            it.root.isThread().shouldBeTrue()
+            it.root.isTextMessage().shouldBeTrue()
+            it.root.getRootThreadEventId()?.shouldBeEqualTo(selectedInitMessage.root.eventId.orEmpty()) ?: assert(false)
+        }
+
+        // The init normal message should now be a root thread event
+        val timeline = aliceRoom.createTimeline(null, TimelineSettings(30))
+        timeline.start()
+
+        aliceSession.startSync(true)
+        run {
+            val lock = CountDownLatch(1)
+            val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
+                val initMessageThreadDetails = snapshot.firstOrNull { it.root.eventId == selectedInitMessage.root.eventId }?.root?.threadDetails
+                // Selected init message should be the thread root
+                initMessageThreadDetails?.isRootThread?.shouldBeTrue()
+                // All threads should be 40
+                initMessageThreadDetails?.numberOfThreads?.shouldBeEqualTo(40)
+                true
+            }
+            // Because we sent more than 30 messages we should paginate a bit more
+            timeline.paginate(Timeline.Direction.BACKWARDS, 50)
+            timeline.addListener(eventsListener)
+            commonTestHelper.await(lock, 600_000)
+        }
+        aliceSession.stopSync()
+    }
+
+    @Test
+    fun thread_summary_advanced_validation_after_multiple_messages_in_multiple_threads() {
+        val commonTestHelper = CommonTestHelper(context())
+        val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
+        val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
+
+        val aliceSession = cryptoTestData.firstSession
+        val aliceRoomId = cryptoTestData.roomId
+
+        val aliceRoom = aliceSession.getRoom(aliceRoomId)!!
+
+        // Let's send 5 messages in the normal timeline
+        val textMessage = "This is a normal timeline message"
+        val sentMessages = commonTestHelper.sendTextMessage(
+                room = aliceRoom,
+                message = textMessage,
+                nbOfMessages = 5)
+
+        sentMessages.forEach {
+            it.root.isThread().shouldBeFalse()
+            it.root.isTextMessage().shouldBeTrue()
+            it.root.getRootThreadEventId().shouldBeNull()
+            it.root.threadDetails?.isRootThread?.shouldBeFalse()
+        }
+        // let's start the thread from the second message
+        val firstMessage = sentMessages[0]
+        val secondMessage = sentMessages[1]
+
+        // Alice will reply in thread to the second message 35 times
+        val aliceThreadRepliesInSecondMessage = commonTestHelper.replyInThreadMessage(
+                room = aliceRoom,
+                message = "Alice reply In the above second thread message",
+                numberOfMessages = 35,
+                rootThreadEventId = secondMessage.root.eventId.orEmpty())
+
+        // Let's reply in timeline to that message from another user
+        val bobSession = cryptoTestData.secondSession!!
+        val bobRoomId = cryptoTestData.roomId
+        val bobRoom = bobSession.getRoom(bobRoomId)!!
+
+        // Bob will reply in thread to the first message 35 times
+        val bobThreadRepliesInFirstMessage = commonTestHelper.replyInThreadMessage(
+                room = bobRoom,
+                message = "Bob reply In the above first thread message",
+                numberOfMessages = 42,
+                rootThreadEventId = firstMessage.root.eventId.orEmpty())
+
+        // Bob will also reply in second thread 5 times
+        val bobThreadRepliesInSecondMessage = commonTestHelper.replyInThreadMessage(
+                room = bobRoom,
+                message = "Another Bob reply In the above second thread message",
+                numberOfMessages = 20,
+                rootThreadEventId = secondMessage.root.eventId.orEmpty())
+
+        aliceThreadRepliesInSecondMessage.forEach {
+            it.root.isThread().shouldBeTrue()
+            it.root.isTextMessage().shouldBeTrue()
+            it.root.getRootThreadEventId()?.shouldBeEqualTo(secondMessage.root.eventId.orEmpty()) ?: assert(false)
+        }
+
+        bobThreadRepliesInFirstMessage.forEach {
+            it.root.isThread().shouldBeTrue()
+            it.root.isTextMessage().shouldBeTrue()
+            it.root.getRootThreadEventId()?.shouldBeEqualTo(firstMessage.root.eventId.orEmpty()) ?: assert(false)
+        }
+
+        bobThreadRepliesInSecondMessage.forEach {
+            it.root.isThread().shouldBeTrue()
+            it.root.isTextMessage().shouldBeTrue()
+            it.root.getRootThreadEventId()?.shouldBeEqualTo(secondMessage.root.eventId.orEmpty()) ?: assert(false)
+        }
+
+        // The init normal message should now be a root thread event
+        val timeline = aliceRoom.createTimeline(null, TimelineSettings(30))
+        timeline.start()
+
+        aliceSession.startSync(true)
+        run {
+            val lock = CountDownLatch(1)
+            val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
+                val firstMessageThreadDetails = snapshot.firstOrNull { it.root.eventId == firstMessage.root.eventId }?.root?.threadDetails
+                val secondMessageThreadDetails = snapshot.firstOrNull { it.root.eventId == secondMessage.root.eventId }?.root?.threadDetails
+
+                // first & second message should be the thread root
+                firstMessageThreadDetails?.isRootThread?.shouldBeTrue()
+                secondMessageThreadDetails?.isRootThread?.shouldBeTrue()
+
+                // First thread message should contain 42
+                firstMessageThreadDetails?.numberOfThreads shouldBeEqualTo 42
+                // Second thread message should contain 35+20
+                secondMessageThreadDetails?.numberOfThreads shouldBeEqualTo 55
+
+                true
+            }
+            // Because we sent more than 30 messages we should paginate a bit more
+            timeline.paginate(Timeline.Direction.BACKWARDS, 50)
+            timeline.paginate(Timeline.Direction.BACKWARDS, 50)
+            timeline.addListener(eventsListener)
+            commonTestHelper.await(lock, 600_000)
+        }
+        aliceSession.stopSync()
+    }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt
index 5fbfaf99a0ed9f2f0f414ed8e3bbf87357fe7a1f..20faa81bb6eb6f3b0404edbba14d2773ee2f4313 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt
@@ -344,7 +344,6 @@ class SpaceHierarchyTest : InstrumentedTest {
         // Test part one of the rooms
 
         val bRoomId = spaceBInfo.roomIds.first()
-        val bRoom = session.getRoom(bRoomId)
 
         commonTestHelper.waitWithLatch { latch ->
             val flatAChildren = session.getFlattenRoomSummaryChildrenOfLive(spaceAInfo.spaceId)
@@ -360,7 +359,7 @@ class SpaceHierarchyTest : InstrumentedTest {
             }
 
             // part from b room
-            bRoom!!.leave(null)
+            session.leaveRoom(bRoomId)
             // The room should have disapear from flat children
             flatAChildren.observeForever(childObserver)
         }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt
index 901ba75d169738aed6cc8d97fbf73d9dfdd88b8d..5fedff53f0d62b10e0167e73824da5fe10f992e7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt
@@ -99,12 +99,31 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
         private lateinit var instance: Matrix
         private val isInit = AtomicBoolean(false)
 
+        /**
+         * Creates a new instance of Matrix, it's recommended to manage this instance as a singleton.
+         * To make use of the built in singleton use Matrix.initialize() and/or Matrix.getInstance(context) instead
+         **/
+        fun createInstance(context: Context, matrixConfiguration: MatrixConfiguration): Matrix {
+            return Matrix(context.applicationContext, matrixConfiguration)
+        }
+
+        /**
+         * Initializes a singleton instance of Matrix for the given MatrixConfiguration
+         * This instance will be returned by Matrix.getInstance(context)
+         */
+        @Deprecated("Use Matrix.createInstance and manage the instance manually")
         fun initialize(context: Context, matrixConfiguration: MatrixConfiguration) {
             if (isInit.compareAndSet(false, true)) {
                 instance = Matrix(context.applicationContext, matrixConfiguration)
             }
         }
 
+        /**
+         * Either provides an already initialized singleton Matrix instance or queries the application context for a MatrixConfiguration.Provider
+         * to lazily create and store the instance.
+         */
+        @Suppress("deprecation") // suppressing warning as this method is unused but is still provided for SDK clients
+        @Deprecated("Use Matrix.createInstance and manage the instance manually")
         fun getInstance(context: Context): Matrix {
             if (isInit.compareAndSet(false, true)) {
                 val appContext = context.applicationContext
@@ -113,7 +132,8 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
                     instance = Matrix(appContext, matrixConfiguration)
                 } else {
                     throw IllegalStateException("Matrix is not initialized properly." +
-                            " You should call Matrix.initialize or let your application implements MatrixConfiguration.Provider.")
+                            " If you want to manage your own Matrix instance use Matrix.createInstance" +
+                            " otherwise you should call Matrix.initialize or let your application implement MatrixConfiguration.Provider.")
                 }
             }
             return instance
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt
index 306ed455003a78f3f1d22500677934568be658fc..c87f21d7ac3a5b1d6e475c855e2ab083848a085e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt
@@ -66,6 +66,7 @@ data class MatrixConfiguration(
     /**
      * Can be implemented by your Application class.
      */
+    @Deprecated("Use Matrix.createInstance and manage the instance manually instead of Matrix.getInstance")
     interface Provider {
         fun providesMatrixConfiguration(): MatrixConfiguration
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt
index 88268f0f86e682fb735ffd66abeea99a22976ba0..76885d85454e5e0401273f438448a3e4785439ab 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt
@@ -50,6 +50,9 @@ interface PushRuleService {
 
 //    fun fulfilledBingRule(event: Event, rules: List<PushRule>): PushRule?
 
+    fun resolveSenderNotificationPermissionCondition(event: Event,
+                                                     condition: SenderNotificationPermissionCondition): Boolean
+
     interface PushRuleListener {
         fun onEvents(pushEvents: PushEvents)
     }
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 aad5fce33ecc3ab849136dd9730b0ee12b23af2f..df57ca568111d9c5144ee77e168d440dfd8a96f3 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
@@ -25,9 +25,14 @@ import org.matrix.android.sdk.api.session.crypto.MXCryptoError
 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.model.message.MessageContent
+import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
+import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
 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.model.relation.shouldRenderInThread
 import org.matrix.android.sdk.api.session.room.send.SendState
+import org.matrix.android.sdk.api.session.threads.ThreadDetails
+import org.matrix.android.sdk.api.util.ContentUtils
 import org.matrix.android.sdk.api.util.JsonDict
 import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
 import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
@@ -98,6 +103,9 @@ data class Event(
     @Transient
     var sendStateDetails: String? = null
 
+    @Transient
+    var threadDetails: ThreadDetails? = null
+
     fun sendStateError(): MatrixError? {
         return sendStateDetails?.let {
             val matrixErrorAdapter = MoshiProvider.providesMoshi().adapter(MatrixError::class.java)
@@ -123,6 +131,7 @@ data class Event(
             it.mCryptoErrorReason = mCryptoErrorReason
             it.sendState = sendState
             it.ageLocalTs = ageLocalTs
+            it.threadDetails = threadDetails
         }
     }
 
@@ -185,6 +194,51 @@ data class Event(
         return contentMap?.let { JSONObject(adapter.toJson(it)).toString(4) }
     }
 
+    /**
+     * Returns a user friendly content depending on the message type.
+     * It can be used especially for message summaries.
+     * It will return a decrypted text message or an empty string otherwise.
+     */
+    fun getDecryptedTextSummary(): String? {
+        if (isRedacted()) return "Message Deleted"
+        val text = getDecryptedValue() ?: return null
+        return when {
+            isReplyRenderedInThread() || isQuote() -> ContentUtils.extractUsefulTextFromReply(text)
+            isFileMessage()                        -> "sent a file."
+            isAudioMessage()                       -> "sent an audio file."
+            isImageMessage()                       -> "sent an image."
+            isVideoMessage()                       -> "sent a video."
+            isSticker()                            -> "sent a sticker"
+            isPoll()                               -> getPollQuestion() ?: "created a poll."
+            else                                   -> text
+        }
+    }
+
+    private fun Event.isQuote(): Boolean {
+        if (isReplyRenderedInThread()) return false
+        return getDecryptedValue("formatted_body")?.contains("<blockquote>") ?: false
+    }
+
+    /**
+     * Determines whether or not current event has mentioned the user
+     */
+    fun isUserMentioned(userId: String): Boolean {
+        return getDecryptedValue("formatted_body")?.contains(userId) ?: false
+    }
+
+    /**
+     * Decrypt the message, or return the pure payload value if there is no encryption
+     */
+    private fun getDecryptedValue(key: String = "body"): String? {
+        return if (isEncrypted()) {
+            @Suppress("UNCHECKED_CAST")
+            val decryptedContent = mxDecryptionResult?.payload?.get("content") as? JsonDict
+            decryptedContent?.get(key) as? String
+        } else {
+            content?.get(key) as? String
+        }
+    }
+
     /**
      * Tells if the event is redacted
      */
@@ -217,7 +271,7 @@ data class Event(
         if (mCryptoError != other.mCryptoError) return false
         if (mCryptoErrorReason != other.mCryptoErrorReason) return false
         if (sendState != other.sendState) return false
-
+        if (threadDetails != other.threadDetails) return false
         return true
     }
 
@@ -236,6 +290,8 @@ data class Event(
         result = 31 * result + (mCryptoError?.hashCode() ?: 0)
         result = 31 * result + (mCryptoErrorReason?.hashCode() ?: 0)
         result = 31 * result + sendState.hashCode()
+        result = 31 * result + threadDetails.hashCode()
+
         return result
     }
 }
@@ -243,70 +299,101 @@ data class Event(
 fun Event.isTextMessage(): Boolean {
     return getClearType() == EventType.MESSAGE &&
             when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) {
-        MessageType.MSGTYPE_TEXT,
-        MessageType.MSGTYPE_EMOTE,
-        MessageType.MSGTYPE_NOTICE -> true
-        else                       -> false
-    }
+                MessageType.MSGTYPE_TEXT,
+                MessageType.MSGTYPE_EMOTE,
+                MessageType.MSGTYPE_NOTICE -> true
+                else                       -> false
+            }
 }
 
 fun Event.isImageMessage(): Boolean {
     return getClearType() == EventType.MESSAGE &&
             when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) {
-        MessageType.MSGTYPE_IMAGE -> true
-        else                      -> false
-    }
+                MessageType.MSGTYPE_IMAGE -> true
+                else                      -> false
+            }
 }
 
 fun Event.isVideoMessage(): Boolean {
     return getClearType() == EventType.MESSAGE &&
             when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) {
-        MessageType.MSGTYPE_VIDEO -> true
-        else                      -> false
-    }
+                MessageType.MSGTYPE_VIDEO -> true
+                else                      -> false
+            }
 }
 
 fun Event.isAudioMessage(): Boolean {
     return getClearType() == EventType.MESSAGE &&
             when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) {
-        MessageType.MSGTYPE_AUDIO -> true
-        else                      -> false
-    }
+                MessageType.MSGTYPE_AUDIO -> true
+                else                      -> false
+            }
 }
 
 fun Event.isFileMessage(): Boolean {
     return getClearType() == EventType.MESSAGE &&
             when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) {
-        MessageType.MSGTYPE_FILE -> true
-        else                     -> false
-    }
+                MessageType.MSGTYPE_FILE -> true
+                else                     -> false
+            }
 }
 
 fun Event.isAttachmentMessage(): Boolean {
     return getClearType() == EventType.MESSAGE &&
             when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) {
-        MessageType.MSGTYPE_IMAGE,
-        MessageType.MSGTYPE_AUDIO,
-        MessageType.MSGTYPE_VIDEO,
-        MessageType.MSGTYPE_FILE -> true
-        else                     -> false
-    }
+                MessageType.MSGTYPE_IMAGE,
+                MessageType.MSGTYPE_AUDIO,
+                MessageType.MSGTYPE_VIDEO,
+                MessageType.MSGTYPE_FILE -> true
+                else                     -> false
+            }
 }
 
+fun Event.isPoll(): Boolean = getClearType() == EventType.POLL_START || getClearType() == EventType.POLL_END
+
+fun Event.isSticker(): Boolean = getClearType() == EventType.STICKER
+
 fun Event.getRelationContent(): RelationDefaultContent? {
     return if (isEncrypted()) {
         content.toModel<EncryptedEventContent>()?.relatesTo
     } else {
-        content.toModel<MessageContent>()?.relatesTo
+        content.toModel<MessageContent>()?.relatesTo ?: run {
+            // Special case to handle stickers, while there is only a local msgtype for stickers
+            if (getClearType() == EventType.STICKER) {
+                getClearContent().toModel<MessageStickerContent>()?.relatesTo
+            } else {
+                null
+            }
+        }
     }
 }
 
+/**
+ * Returns the poll question or null otherwise
+ */
+fun Event.getPollQuestion(): String? =
+        getPollContent()?.pollCreationInfo?.question?.question
+
+/**
+ * Returns the relation content for a specific type or null otherwise
+ */
+fun Event.getRelationContentForType(type: String): RelationDefaultContent? =
+        getRelationContent()?.takeIf { it.type == type }
+
 fun Event.isReply(): Boolean {
     return getRelationContent()?.inReplyTo?.eventId != null
 }
 
+fun Event.isReplyRenderedInThread(): Boolean {
+    return isReply() && getRelationContent()?.inReplyTo?.shouldRenderInThread() == true
+}
+
+fun Event.isThread(): Boolean = getRelationContentForType(RelationType.IO_THREAD)?.eventId != null
+
+fun Event.getRootThreadEventId(): String? = getRelationContentForType(RelationType.IO_THREAD)?.eventId
+
 fun Event.isEdition(): Boolean {
-    return getRelationContent()?.takeIf { it.type == RelationType.REPLACE }?.eventId != null
+    return getRelationContentForType(RelationType.REPLACE)?.eventId != null
 }
 
 fun Event.getPresenceContent(): PresenceContent? {
@@ -315,3 +402,7 @@ fun Event.getPresenceContent(): PresenceContent? {
 
 fun Event.isInvitation(): Boolean = type == EventType.STATE_ROOM_MEMBER &&
         content?.toModel<RoomMemberContent>()?.membership == Membership.INVITE
+
+fun Event.getPollContent(): MessagePollContent? {
+    return content.toModel<MessagePollContent>()
+}
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 f67efc50ba568d62193655b45b74e22fe2597723..fb26264ad7708992935a5fe0dbdefb363af30964 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt
@@ -28,9 +28,9 @@ object RelationType {
     /** Lets you define an event which references an existing event.*/
     const val REFERENCE = "m.reference"
 
-    /** Lets you define an thread event that belongs to another existing event.*/
-//    const val THREAD = "m.thread"         // m.thread is not yet released in the backend
-    const val THREAD = "io.element.thread"  // io.element.thread will be replaced by m.thread when it is released
+    /** Lets you define an event which 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/api/session/homeserver/HomeServerCapabilities.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt
index 3ed6a7ebb2d4536922f7932dd37accf07729acd7..2256dfb8f0c52d0168daebf7e03727e90a72f7ab 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
@@ -21,6 +21,18 @@ data class HomeServerCapabilities(
          * True if it is possible to change the password of the account.
          */
         val canChangePassword: Boolean = true,
+        /**
+         * True if it is possible to change the display name of the account.
+         */
+        val canChangeDisplayName: Boolean = true,
+        /**
+         * True if it is possible to change the avatar of the account.
+         */
+        val canChangeAvatar: Boolean = true,
+        /**
+         * True if it is possible to change the 3pid associations of the account.
+         */
+        val canChange3pid: Boolean = true,
         /**
          * Max size of file which can be uploaded to the homeserver in bytes. [MAX_UPLOAD_FILE_SIZE_UNKNOWN] if unknown or not retrieved yet
          */
@@ -76,6 +88,7 @@ data class HomeServerCapabilities(
             }
         }
     }
+
     fun isFeatureSupported(feature: String, byRoomVersion: String): Boolean {
         if (roomVersions?.capabilities == null) return false
         val info = roomVersions.capabilities[feature] ?: return false
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/PreviewUrlData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/PreviewUrlData.kt
index 33fc8b052b0616681d10ce4dfa14e86ed929a931..bfba43a82d2cb8d2e4cb8c1c44b20040d9081f70 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/PreviewUrlData.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/PreviewUrlData.kt
@@ -47,5 +47,9 @@ data class PreviewUrlData(
         // Value of field "og:description"
         val description: String?,
         // Value of field "og:image"
-        val mxcUrl: String?
+        val mxcUrl: String?,
+        // Value of field "og:image:width"
+        val imageWidth: Int?,
+        // Value of field "og:image:height"
+        val imageHeight: Int?
 )
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 6c0e730499468b4139fbccd9e69d610efb690887..d930a5d0fd6cb4d392c2097b8ef7b1afa9bdb141 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
@@ -32,6 +32,7 @@ import org.matrix.android.sdk.api.session.room.send.DraftService
 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.timeline.TimelineService
 import org.matrix.android.sdk.api.session.room.typing.TypingService
 import org.matrix.android.sdk.api.session.room.uploads.UploadsService
@@ -45,6 +46,7 @@ import org.matrix.android.sdk.api.util.Optional
  */
 interface Room :
         TimelineService,
+        ThreadsService,
         SendService,
         DraftService,
         ReadService,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt
index e4bd498990eeb35f61a200e48cdad28d876b82e5..bca432320d92d456095240ef912163a392f30680 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt
@@ -76,6 +76,13 @@ interface RoomService {
             thirdPartySigned: SignInvitationResult
     )
 
+    /**
+     * Leave the room, or reject an invitation.
+     * @param roomId the roomId of the room to leave
+     * @param reason optional reason for leaving the room
+     */
+    suspend fun leaveRoom(roomId: String, reason: String? = null)
+
     /**
      * Get a room from a roomId
      * @param roomId the roomId to look for.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/MembershipService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/MembershipService.kt
index d5bc65c14294de2aea842af5c2161773fee92410..6c8e2d310c0777591f056d223652223635e7941d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/MembershipService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/MembershipService.kt
@@ -81,14 +81,4 @@ interface MembershipService {
 
     @Deprecated("Use remove instead", ReplaceWith("remove(userId, reason)"))
     suspend fun kick(userId: String, reason: String? = null) = remove(userId, reason)
-
-    /**
-     * Join the room, or accept an invitation.
-     */
-    suspend fun join(reason: String? = null, viaServers: List<String> = emptyList())
-
-    /**
-     * Leave the room, or reject an invitation.
-     */
-    suspend fun leave(reason: String? = null)
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReadReceipt.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReadReceipt.kt
index 67cb9600c82dce5319b270c1c9198846c23c197b..5639730219e6d912c1a39f6d3e592a26a4588c20 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReadReceipt.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReadReceipt.kt
@@ -16,9 +16,7 @@
 
 package org.matrix.android.sdk.api.session.room.model
 
-import org.matrix.android.sdk.api.session.user.model.User
-
 data class ReadReceipt(
-        val user: User,
+        val roomMember: RoomMemberSummary,
         val originServerTs: Long
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt
index c090487c58ffabd7a4c31e7dc01abe76ff0b99f4..d07bd2d73ae5df8f6f8ad1e853f11881cbd26636 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt
@@ -64,4 +64,12 @@ data class MessageLocationContent(
 ) : MessageContent {
 
     fun getBestGeoUri() = locationInfo?.geoUri ?: geoUri
+
+    /**
+     * @return true if the location asset is a user location, not a generic one.
+     */
+    fun isSelfLocation(): Boolean {
+        // Should behave like m.self if locationAsset is null
+        return locationAsset?.type == null || locationAsset.type == LocationAssetType.SELF
+    }
 }
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 763d4bb89294eaff6683ad0376922cc05cae4fec..09114436f04075cd9401717c4af0bd6a39e7961b 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
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.room.model.relation
 import androidx.lifecycle.LiveData
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
+import org.matrix.android.sdk.api.session.room.model.message.MessageType
 import org.matrix.android.sdk.api.session.room.model.message.PollType
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
 import org.matrix.android.sdk.api.util.Cancelable
@@ -45,6 +46,9 @@ import org.matrix.android.sdk.api.util.Optional
  *  m.reference - lets you define an event which references an existing event.
  *              When aggregated, currently doesn't do anything special, but in future could bundle chains of references (i.e. threads).
  *              These are primarily intended for handling replies (and in future threads).
+ *
+ *  m.thread - lets you define an event which is a thread reply to an existing event.
+ *             When aggregated, returns the most thread event
  */
 interface RelationService {
 
@@ -62,8 +66,8 @@ interface RelationService {
      * @param targetEventId the id of the event being reacted
      * @param reaction the reaction (preferably emoji)
      */
-    fun undoReaction(targetEventId: String,
-                     reaction: String): Cancelable
+    suspend fun undoReaction(targetEventId: String,
+                             reaction: String): Cancelable
 
     /**
      * Edit a poll.
@@ -118,10 +122,15 @@ interface RelationService {
      * @param eventReplied the event referenced by the reply
      * @param replyText the reply text
      * @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
+     * @param showInThread If true, relation will be added to the reply in order to be visible from within threads
+     * @param rootThreadEventId If show in thread is true then we need the rootThreadEventId to generate the relation
      */
     fun replyToMessage(eventReplied: TimelineEvent,
                        replyText: CharSequence,
-                       autoMarkdown: Boolean = false): Cancelable?
+                       autoMarkdown: Boolean = false,
+                       showInThread: Boolean = false,
+                       rootThreadEventId: String? = null
+    ): Cancelable?
 
     /**
      * Get the current EventAnnotationsSummary
@@ -136,4 +145,31 @@ interface RelationService {
      * @return the LiveData of EventAnnotationsSummary
      */
     fun getEventAnnotationsSummaryLive(eventId: String): LiveData<Optional<EventAnnotationsSummary>>
+
+    /**
+     * Creates a thread reply for an existing timeline event
+     * The replyInThreadText can be a Spannable and contains special spans (MatrixItemSpan) that will be translated
+     * by the sdk into pills.
+     * @param rootThreadEventId the root thread eventId
+     * @param replyInThreadText the reply text
+     * @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE
+     * @param formattedText The formatted body using MessageType#FORMAT_MATRIX_HTML
+     * @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
+     * @param eventReplied the event referenced by the reply within a thread
+     */
+    fun replyInThread(rootThreadEventId: String,
+                      replyInThreadText: CharSequence,
+                      msgType: String = MessageType.MSGTYPE_TEXT,
+                      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/model/relation/ReplyToContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReplyToContent.kt
index 251328bea210065be3c79c58633453f935899f78..412a1bfca9dcec11ab88a5d97520cc8f4aecc974 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,5 +21,8 @@ import com.squareup.moshi.JsonClass
 
 @JsonClass(generateAdapter = true)
 data class ReplyToContent(
-        @Json(name = "event_id") val eventId: String? = null
+        @Json(name = "event_id") val eventId: String? = null,
+        @Json(name = "render_in") val renderIn: List<String>? = null
 )
+
+fun ReplyToContent.shouldRenderInThread(): Boolean = renderIn?.contains("m.thread") == true
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt
index 20d00394df48ae29c03e2650c67630b146bea1a9..913dbfd01096adf5f93ffbaa3751e4fd705a6206 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt
@@ -64,7 +64,7 @@ interface SendService {
      * @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
      * @return a [Cancelable]
      */
-    fun sendQuotedTextMessage(quotedEvent: TimelineEvent, text: String, autoMarkdown: Boolean): Cancelable
+    fun sendQuotedTextMessage(quotedEvent: TimelineEvent, text: String, autoMarkdown: Boolean, rootThreadEventId: String? = null): Cancelable
 
     /**
      * Method to send a media asynchronously.
@@ -72,11 +72,13 @@ interface SendService {
      * @param compressBeforeSending set to true to compress images before sending them
      * @param roomIds set of roomIds to where the media will be sent. The current roomId will be add to this set if not present.
      *                It can be useful to send media to multiple room. It's safe to include the current roomId in this set
+     * @param rootThreadEventId when this param is not null, the Media will be sent in this specific thread
      * @return a [Cancelable]
      */
     fun sendMedia(attachment: ContentAttachmentData,
                   compressBeforeSending: Boolean,
-                  roomIds: Set<String>): Cancelable
+                  roomIds: Set<String>,
+                  rootThreadEventId: String? = null): Cancelable
 
     /**
      * Method to send a list of media asynchronously.
@@ -84,11 +86,13 @@ interface SendService {
      * @param compressBeforeSending set to true to compress images before sending them
      * @param roomIds set of roomIds to where the media will be sent. The current roomId will be add to this set if not present.
      *                It can be useful to send media to multiple room. It's safe to include the current roomId in this set
+     * @param rootThreadEventId when this param is not null, all the Media will be sent in this specific thread
      * @return a [Cancelable]
      */
     fun sendMedias(attachments: List<ContentAttachmentData>,
                    compressBeforeSending: Boolean,
-                   roomIds: Set<String>): Cancelable
+                   roomIds: Set<String>,
+                   rootThreadEventId: String? = null): Cancelable
 
     /**
      * Send a poll to the room.
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
new file mode 100644
index 0000000000000000000000000000000000000000..e4d1d979e1a8f8c61b55fa8afc5a99b58ee8a040
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt
@@ -0,0 +1,67 @@
+/*
+ * 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
+
+import androidx.lifecycle.LiveData
+import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+
+/**
+ * This interface defines methods to interact with threads related features.
+ * It's implemented at the room level within the main timeline.
+ */
+interface ThreadsService {
+
+    /**
+     * 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/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt
index 241e5f3b9b8808fcece17a52a101640bd7aba661..d47a656798c45df853a592c8f04f6370d28f1bd2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt
@@ -43,7 +43,7 @@ interface Timeline {
     /**
      * This must be called before any other method after creating the timeline. It ensures the underlying database is open
      */
-    fun start()
+    fun start(rootThreadEventId: String? = null)
 
     /**
      * This must be called when you don't need the timeline. It ensures the underlying database get closed.
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 3f7d2d127888c7d00556636f0cdf553c1b30c9fc..6f8bae876bded065bf675a015ee7b44bc3d1dfc4 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
@@ -22,7 +22,9 @@ import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.RelationType
 import org.matrix.android.sdk.api.session.events.model.getRelationContent
 import org.matrix.android.sdk.api.session.events.model.isEdition
+import org.matrix.android.sdk.api.session.events.model.isPoll
 import org.matrix.android.sdk.api.session.events.model.isReply
+import org.matrix.android.sdk.api.session.events.model.isSticker
 import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
 import org.matrix.android.sdk.api.session.room.model.ReadReceipt
@@ -149,6 +151,13 @@ fun TimelineEvent.isEdition(): Boolean {
     return root.isEdition()
 }
 
+fun TimelineEvent.isPoll(): Boolean =
+        root.isPoll()
+
+fun TimelineEvent.isSticker(): Boolean {
+    return root.isSticker()
+}
+
 /**
  * Get the latest message body, after a possible edition, stripping the reply prefix if necessary
  */
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt
index ceffedb234775e286efa12d53e5bf5e4e1e3caee..6548453c8a126da8f203c82957ed46083781f59d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt
@@ -27,5 +27,14 @@ data class TimelineSettings(
         /**
          * If true, will build read receipts for each event.
          */
-        val buildReadReceipts: Boolean = true
-)
+        val buildReadReceipts: Boolean = true,
+        /**
+         * The root thread eventId if this is a thread timeline, or null if this is NOT a thread timeline
+         */
+        val rootThreadEventId: String? = null) {
+
+    /**
+     * Returns true if this is a thread timeline or false otherwise
+     */
+    fun isThreadTimeline() = rootThreadEventId != null
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/Space.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/Space.kt
index 207050be7d965a689ecf5554bfc66c77ba770f04..f0ed9daac5dc651cace526cb13484736e84e7eff 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/Space.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/Space.kt
@@ -26,8 +26,6 @@ interface Space {
 
     val spaceId: String
 
-    suspend fun leave(reason: String? = null)
-
     /**
      * A current snapshot of [RoomSummary] associated with the space
      */
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt
index 357c0b941a4d60fc40b034f4d02fa05209d03632..41c4e7eed1802a6d5384cc36a2cd0fb066769866 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt
@@ -87,6 +87,13 @@ interface SpaceService {
 
     suspend fun rejectInvite(spaceId: String, reason: String?)
 
+    /**
+     * Leave the space, or reject an invitation.
+     * @param spaceId the spaceId of the space to leave
+     * @param reason optional reason for leaving the space
+     */
+    suspend fun leaveSpace(spaceId: String, reason: String? = null)
+
 //    fun getSpaceParentsOfRoom(roomId: String) : List<SpaceSummary>
 
     /**
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
new file mode 100644
index 0000000000000000000000000000000000000000..fafe17b2c0957261a1992858f89344d1e65b5dac
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session.threads
+
+import org.matrix.android.sdk.api.session.room.sender.SenderInfo
+
+/**
+ * This class contains all the details needed for threads.
+ * Is is mainly used from within an Event.
+ */
+data class ThreadDetails(
+        val isRootThread: Boolean = false,
+        val numberOfThreads: Int = 0,
+        val threadSummarySenderInfo: SenderInfo? = null,
+        val threadSummaryLatestTextMessage: String? = null,
+        val lastMessageTimestamp: Long? = null,
+        var threadNotificationState: ThreadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE,
+        val isThread: Boolean = false,
+        val lastRootThreadEdition: String? = null
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadNotificationBadgeState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadNotificationBadgeState.kt
new file mode 100644
index 0000000000000000000000000000000000000000..8e861e73de1a279946c35d68aa8e7aab00ba3655
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadNotificationBadgeState.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session.threads
+
+/**
+ * This class defines the state of a thread notification badge
+ */
+data class ThreadNotificationBadgeState(
+        val numberOfLocalUnreadThreads: Int = 0,
+        val isUserMentioned: Boolean = false
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadNotificationState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadNotificationState.kt
new file mode 100644
index 0000000000000000000000000000000000000000..8566d68aa52ab389dd4a89f9d6678e110fc795ba
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadNotificationState.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session.threads
+
+/**
+ * This class defines the state of a thread notification
+ */
+enum class ThreadNotificationState {
+
+    // There are no new message
+    NO_NEW_MESSAGE,
+
+    // There is at least one new message
+    NEW_MESSAGE,
+
+    // The is at least one new message that should be highlighted
+    // ex. "Hello @aris.kotsomitopoulos"
+    NEW_HIGHLIGHTED_MESSAGE;
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadTimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadTimelineEvent.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7b433566b8df2029f6a112e52ef121ab76a6ef07
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadTimelineEvent.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session.threads
+
+import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+
+/**
+ * This class contains a thread TimelineEvent along with a boolean that
+ * determines if the current user has participated in that event
+ */
+data class ThreadTimelineEvent(
+        val timelineEvent: TimelineEvent,
+        val isParticipating: Boolean
+)
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 3396c4a6c98d814521b43f74c3fc6904b967f15b..302f7387fad7363c72988c1a307f33a13bb4f1f8 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
@@ -35,7 +35,19 @@ sealed class MatrixItem(
     data class UserItem(override val id: String,
                         override val displayName: String? = null,
                         override val avatarUrl: String? = null) :
-        MatrixItem(id, displayName?.removeSuffix(ircPattern), avatarUrl) {
+            MatrixItem(id, displayName?.removeSuffix(IRC_PATTERN), avatarUrl) {
+        init {
+            if (BuildConfig.DEBUG) checkId()
+        }
+
+        override fun updateAvatar(newAvatar: String?) = copy(avatarUrl = newAvatar)
+    }
+
+    data class EveryoneInRoomItem(override val id: String,
+                                  override val displayName: String = NOTIFY_EVERYONE,
+                                  override val avatarUrl: String? = null,
+                                  val roomDisplayName: String? = null) :
+            MatrixItem(id, displayName, avatarUrl) {
         init {
             if (BuildConfig.DEBUG) checkId()
         }
@@ -46,7 +58,7 @@ sealed class MatrixItem(
     data class EventItem(override val id: String,
                          override val displayName: String? = null,
                          override val avatarUrl: String? = null) :
-        MatrixItem(id, displayName, avatarUrl) {
+            MatrixItem(id, displayName, avatarUrl) {
         init {
             if (BuildConfig.DEBUG) checkId()
         }
@@ -57,7 +69,7 @@ sealed class MatrixItem(
     data class RoomItem(override val id: String,
                         override val displayName: String? = null,
                         override val avatarUrl: String? = null) :
-        MatrixItem(id, displayName, avatarUrl) {
+            MatrixItem(id, displayName, avatarUrl) {
         init {
             if (BuildConfig.DEBUG) checkId()
         }
@@ -68,7 +80,7 @@ sealed class MatrixItem(
     data class SpaceItem(override val id: String,
                          override val displayName: String? = null,
                          override val avatarUrl: String? = null) :
-        MatrixItem(id, displayName, avatarUrl) {
+            MatrixItem(id, displayName, avatarUrl) {
         init {
             if (BuildConfig.DEBUG) checkId()
         }
@@ -79,7 +91,7 @@ sealed class MatrixItem(
     data class RoomAliasItem(override val id: String,
                              override val displayName: String? = null,
                              override val avatarUrl: String? = null) :
-        MatrixItem(id, displayName, avatarUrl) {
+            MatrixItem(id, displayName, avatarUrl) {
         init {
             if (BuildConfig.DEBUG) checkId()
         }
@@ -90,7 +102,7 @@ sealed class MatrixItem(
     data class GroupItem(override val id: String,
                          override val displayName: String? = null,
                          override val avatarUrl: String? = null) :
-        MatrixItem(id, displayName, avatarUrl) {
+            MatrixItem(id, displayName, avatarUrl) {
         init {
             if (BuildConfig.DEBUG) checkId()
         }
@@ -109,16 +121,22 @@ sealed class MatrixItem(
     /**
      * Return the prefix as defined in the matrix spec (and not extracted from the id)
      */
-    fun getIdPrefix() = when (this) {
-        is UserItem      -> '@'
-        is EventItem     -> '$'
+    private fun getIdPrefix() = when (this) {
+        is UserItem           -> '@'
+        is EventItem          -> '$'
         is SpaceItem,
-        is RoomItem      -> '!'
-        is RoomAliasItem -> '#'
-        is GroupItem     -> '+'
+        is RoomItem,
+        is EveryoneInRoomItem -> '!'
+        is RoomAliasItem      -> '#'
+        is GroupItem          -> '+'
     }
 
     fun firstLetterOfDisplayName(): String {
+        val displayName = when (this) {
+            // use the room display name for the notify everyone item
+            is EveryoneInRoomItem -> roomDisplayName
+            else                  -> displayName
+        }
         return (displayName?.takeIf { it.isNotBlank() } ?: id)
                 .let { dn ->
                     var startIndex = 0
@@ -151,7 +169,8 @@ sealed class MatrixItem(
     }
 
     companion object {
-        private const val ircPattern = " (IRC)"
+        private const val IRC_PATTERN = " (IRC)"
+        const val NOTIFY_EVERYONE = "@room"
     }
 }
 
@@ -171,6 +190,8 @@ fun RoomSummary.toMatrixItem() = if (roomType == RoomType.SPACE) {
 
 fun RoomSummary.toRoomAliasMatrixItem() = MatrixItem.RoomAliasItem(canonicalAlias ?: roomId, displayName, avatarUrl)
 
+fun RoomSummary.toEveryoneInRoomMatrixItem() = MatrixItem.EveryoneInRoomItem(id = roomId, avatarUrl = avatarUrl, roomDisplayName = displayName)
+
 // If no name is available, use room alias as Riot-Web does
 fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name ?: getPrimaryAlias() ?: "", avatarUrl)
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthModule.kt
index bb62dbbfe92cfe8771d7ac173a24d3366077a913..298e116199642fcedd68436976681ccd49f3327c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthModule.kt
@@ -46,7 +46,9 @@ internal abstract class AuthModule {
         @JvmStatic
         @Provides
         @AuthDatabase
-        fun providesRealmConfiguration(context: Context, realmKeysUtils: RealmKeysUtils): RealmConfiguration {
+        fun providesRealmConfiguration(context: Context,
+                                       realmKeysUtils: RealmKeysUtils,
+                                       authRealmMigration: AuthRealmMigration): RealmConfiguration {
             val old = File(context.filesDir, "matrix-sdk-auth")
             if (old.exists()) {
                 old.renameTo(File(context.filesDir, "matrix-sdk-auth.realm"))
@@ -58,8 +60,8 @@ internal abstract class AuthModule {
                     }
                     .name("matrix-sdk-auth.realm")
                     .modules(AuthRealmModule())
-                    .schemaVersion(AuthRealmMigration.SCHEMA_VERSION)
-                    .migration(AuthRealmMigration)
+                    .schemaVersion(authRealmMigration.schemaVersion)
+                    .migration(authRealmMigration)
                     .build()
         }
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/AuthRealmMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/AuthRealmMigration.kt
index c2104690b32dfc187ff7d16c71cf44f79f9f9438..59b6471a058ed54f7baa7731953ccf462a929932 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/AuthRealmMigration.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/AuthRealmMigration.kt
@@ -16,102 +16,31 @@
 
 package org.matrix.android.sdk.internal.auth.db
 
-import android.net.Uri
 import io.realm.DynamicRealm
 import io.realm.RealmMigration
-import org.matrix.android.sdk.api.auth.data.Credentials
-import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
-import org.matrix.android.sdk.api.auth.data.sessionId
-import org.matrix.android.sdk.internal.di.MoshiProvider
+import org.matrix.android.sdk.internal.auth.db.migration.MigrateAuthTo001
+import org.matrix.android.sdk.internal.auth.db.migration.MigrateAuthTo002
+import org.matrix.android.sdk.internal.auth.db.migration.MigrateAuthTo003
+import org.matrix.android.sdk.internal.auth.db.migration.MigrateAuthTo004
 import timber.log.Timber
+import javax.inject.Inject
 
-internal object AuthRealmMigration : RealmMigration {
+internal class AuthRealmMigration @Inject constructor() : RealmMigration {
+    /**
+     * Forces all AuthRealmMigration instances to be equal
+     * Avoids Realm throwing when multiple instances of the migration are set
+     */
+    override fun equals(other: Any?) = other is AuthRealmMigration
+    override fun hashCode() = 4000
 
-    // Current schema version
-    const val SCHEMA_VERSION = 4L
+    val schemaVersion = 4L
 
     override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
         Timber.d("Migrating Auth Realm from $oldVersion to $newVersion")
 
-        if (oldVersion <= 0) migrateTo1(realm)
-        if (oldVersion <= 1) migrateTo2(realm)
-        if (oldVersion <= 2) migrateTo3(realm)
-        if (oldVersion <= 3) migrateTo4(realm)
-    }
-
-    private fun migrateTo1(realm: DynamicRealm) {
-        Timber.d("Step 0 -> 1")
-        Timber.d("Create PendingSessionEntity")
-
-        realm.schema.create("PendingSessionEntity")
-                .addField(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, String::class.java)
-                .setRequired(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, true)
-                .addField(PendingSessionEntityFields.CLIENT_SECRET, String::class.java)
-                .setRequired(PendingSessionEntityFields.CLIENT_SECRET, true)
-                .addField(PendingSessionEntityFields.SEND_ATTEMPT, Integer::class.java)
-                .setRequired(PendingSessionEntityFields.SEND_ATTEMPT, true)
-                .addField(PendingSessionEntityFields.RESET_PASSWORD_DATA_JSON, String::class.java)
-                .addField(PendingSessionEntityFields.CURRENT_SESSION, String::class.java)
-                .addField(PendingSessionEntityFields.IS_REGISTRATION_STARTED, Boolean::class.java)
-                .addField(PendingSessionEntityFields.CURRENT_THREE_PID_DATA_JSON, String::class.java)
-    }
-
-    private fun migrateTo2(realm: DynamicRealm) {
-        Timber.d("Step 1 -> 2")
-        Timber.d("Add boolean isTokenValid in SessionParamsEntity, with value true")
-
-        realm.schema.get("SessionParamsEntity")
-                ?.addField(SessionParamsEntityFields.IS_TOKEN_VALID, Boolean::class.java)
-                ?.transform { it.set(SessionParamsEntityFields.IS_TOKEN_VALID, true) }
-    }
-
-    private fun migrateTo3(realm: DynamicRealm) {
-        Timber.d("Step 2 -> 3")
-        Timber.d("Update SessionParamsEntity primary key, to allow several sessions with the same userId")
-
-        realm.schema.get("SessionParamsEntity")
-                ?.removePrimaryKey()
-                ?.addField(SessionParamsEntityFields.SESSION_ID, String::class.java)
-                ?.setRequired(SessionParamsEntityFields.SESSION_ID, true)
-                ?.transform {
-                    val credentialsJson = it.getString(SessionParamsEntityFields.CREDENTIALS_JSON)
-
-                    val credentials = MoshiProvider.providesMoshi()
-                            .adapter(Credentials::class.java)
-                            .fromJson(credentialsJson)
-
-                    it.set(SessionParamsEntityFields.SESSION_ID, credentials!!.sessionId())
-                }
-                ?.addPrimaryKey(SessionParamsEntityFields.SESSION_ID)
-    }
-
-    private fun migrateTo4(realm: DynamicRealm) {
-        Timber.d("Step 3 -> 4")
-        Timber.d("Update SessionParamsEntity to add HomeServerConnectionConfig.homeServerUriBase value")
-
-        val adapter = MoshiProvider.providesMoshi()
-                .adapter(HomeServerConnectionConfig::class.java)
-
-        realm.schema.get("SessionParamsEntity")
-                ?.transform {
-                    val homeserverConnectionConfigJson = it.getString(SessionParamsEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON)
-
-                    val homeserverConnectionConfig = adapter
-                            .fromJson(homeserverConnectionConfigJson)
-
-                    val homeserverUrl = homeserverConnectionConfig?.homeServerUri?.toString()
-                    // Special case for matrix.org. Old session may use "https://matrix.org", newer one may use
-                    // "https://matrix-client.matrix.org". So fix that here
-                    val alteredHomeserverConnectionConfig =
-                            if (homeserverUrl == "https://matrix.org" || homeserverUrl == "https://matrix-client.matrix.org") {
-                                homeserverConnectionConfig.copy(
-                                        homeServerUri = Uri.parse("https://matrix.org"),
-                                        homeServerUriBase = Uri.parse("https://matrix-client.matrix.org")
-                                )
-                            } else {
-                                homeserverConnectionConfig
-                            }
-                    it.set(SessionParamsEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, adapter.toJson(alteredHomeserverConnectionConfig))
-                }
+        if (oldVersion < 1) MigrateAuthTo001(realm).perform()
+        if (oldVersion < 2) MigrateAuthTo002(realm).perform()
+        if (oldVersion < 3) MigrateAuthTo003(realm).perform()
+        if (oldVersion < 4) MigrateAuthTo004(realm).perform()
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo001.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo001.kt
new file mode 100644
index 0000000000000000000000000000000000000000..627f4e16bc578e2858ddeca9ffc901cc6a3aefbd
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo001.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.auth.db.migration
+
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.internal.auth.db.PendingSessionEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+import timber.log.Timber
+
+class MigrateAuthTo001(realm: DynamicRealm) : RealmMigrator(realm, 1) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        Timber.d("Create PendingSessionEntity")
+
+        realm.schema.create("PendingSessionEntity")
+                .addField(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, String::class.java)
+                .setRequired(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, true)
+                .addField(PendingSessionEntityFields.CLIENT_SECRET, String::class.java)
+                .setRequired(PendingSessionEntityFields.CLIENT_SECRET, true)
+                .addField(PendingSessionEntityFields.SEND_ATTEMPT, Integer::class.java)
+                .setRequired(PendingSessionEntityFields.SEND_ATTEMPT, true)
+                .addField(PendingSessionEntityFields.RESET_PASSWORD_DATA_JSON, String::class.java)
+                .addField(PendingSessionEntityFields.CURRENT_SESSION, String::class.java)
+                .addField(PendingSessionEntityFields.IS_REGISTRATION_STARTED, Boolean::class.java)
+                .addField(PendingSessionEntityFields.CURRENT_THREE_PID_DATA_JSON, String::class.java)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo002.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo002.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6b133f8580d2b28b8a9cccc4bebb7828f98d7057
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo002.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.auth.db.migration
+
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.internal.auth.db.SessionParamsEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+import timber.log.Timber
+
+class MigrateAuthTo002(realm: DynamicRealm) : RealmMigrator(realm, 2) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        Timber.d("Add boolean isTokenValid in SessionParamsEntity, with value true")
+
+        realm.schema.get("SessionParamsEntity")
+                ?.addField(SessionParamsEntityFields.IS_TOKEN_VALID, Boolean::class.java)
+                ?.transform { it.set(SessionParamsEntityFields.IS_TOKEN_VALID, true) }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo003.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo003.kt
new file mode 100644
index 0000000000000000000000000000000000000000..9319ec99871d0dbef652dc8cec9b1f9e5c03806a
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo003.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.auth.db.migration
+
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.api.auth.data.Credentials
+import org.matrix.android.sdk.api.auth.data.sessionId
+import org.matrix.android.sdk.internal.auth.db.SessionParamsEntityFields
+import org.matrix.android.sdk.internal.di.MoshiProvider
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+import timber.log.Timber
+
+class MigrateAuthTo003(realm: DynamicRealm) : RealmMigrator(realm, 3) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        Timber.d("Update SessionParamsEntity primary key, to allow several sessions with the same userId")
+
+        realm.schema.get("SessionParamsEntity")
+                ?.removePrimaryKey()
+                ?.addField(SessionParamsEntityFields.SESSION_ID, String::class.java)
+                ?.setRequired(SessionParamsEntityFields.SESSION_ID, true)
+                ?.transform {
+                    val credentialsJson = it.getString(SessionParamsEntityFields.CREDENTIALS_JSON)
+
+                    val credentials = MoshiProvider.providesMoshi()
+                            .adapter(Credentials::class.java)
+                            .fromJson(credentialsJson)
+
+                    it.set(SessionParamsEntityFields.SESSION_ID, credentials!!.sessionId())
+                }
+                ?.addPrimaryKey(SessionParamsEntityFields.SESSION_ID)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo004.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo004.kt
new file mode 100644
index 0000000000000000000000000000000000000000..4a9b9022d58279bc99bf588d42739a9667c93d9b
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo004.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.auth.db.migration
+
+import android.net.Uri
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
+import org.matrix.android.sdk.internal.auth.db.SessionParamsEntityFields
+import org.matrix.android.sdk.internal.di.MoshiProvider
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+import timber.log.Timber
+
+class MigrateAuthTo004(realm: DynamicRealm) : RealmMigrator(realm, 4) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        Timber.d("Update SessionParamsEntity to add HomeServerConnectionConfig.homeServerUriBase value")
+
+        val adapter = MoshiProvider.providesMoshi()
+                .adapter(HomeServerConnectionConfig::class.java)
+
+        realm.schema.get("SessionParamsEntity")
+                ?.transform {
+                    val homeserverConnectionConfigJson = it.getString(SessionParamsEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON)
+
+                    val homeserverConnectionConfig = adapter
+                            .fromJson(homeserverConnectionConfigJson)
+
+                    val homeserverUrl = homeserverConnectionConfig?.homeServerUri?.toString()
+                    // Special case for matrix.org. Old session may use "https://matrix.org", newer one may use
+                    // "https://matrix-client.matrix.org". So fix that here
+                    val alteredHomeserverConnectionConfig =
+                            if (homeserverUrl == "https://matrix.org" || homeserverUrl == "https://matrix-client.matrix.org") {
+                                homeserverConnectionConfig.copy(
+                                        homeServerUri = Uri.parse("https://matrix.org"),
+                                        homeServerUriBase = Uri.parse("https://matrix-client.matrix.org")
+                                )
+                            } else {
+                                homeserverConnectionConfig
+                            }
+                    it.set(SessionParamsEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, adapter.toJson(alteredHomeserverConnectionConfig))
+                }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt
index fe388b44e22e427f9bf08833e29699a497896eab..3130a6382f1f8dd0346831f87bb18bd57a9d9d4f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt
@@ -112,7 +112,8 @@ internal abstract class CryptoModule {
         @SessionScope
         fun providesRealmConfiguration(@SessionFilesDirectory directory: File,
                                        @UserMd5 userMd5: String,
-                                       realmKeysUtils: RealmKeysUtils): RealmConfiguration {
+                                       realmKeysUtils: RealmKeysUtils,
+                                       realmCryptoStoreMigration: RealmCryptoStoreMigration): RealmConfiguration {
             return RealmConfiguration.Builder()
                     .directory(directory)
                     .apply {
@@ -121,8 +122,8 @@ internal abstract class CryptoModule {
                     .name("crypto_store.realm")
                     .modules(RealmCryptoStoreModule())
                     .allowWritesOnUiThread(true)
-                    .schemaVersion(RealmCryptoStoreMigration.CRYPTO_STORE_SCHEMA_VERSION)
-                    .migration(RealmCryptoStoreMigration)
+                    .schemaVersion(realmCryptoStoreMigration.schemaVersion)
+                    .migration(realmCryptoStoreMigration)
                     .build()
         }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt
index 82eced43711f3cf6c52a4dc43f25d658899ec7be..2a58d731e5310215631eb359362ede7b17426f25 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt
@@ -35,6 +35,8 @@ internal class CryptoSessionInfoProvider @Inject constructor(
 ) {
 
     fun isRoomEncrypted(roomId: String): Boolean {
+        // We look at the presence at any m.room.encryption state event no matter if it's
+        // the latest one or if it is well formed
         val encryptionEvent = monarchy.fetchCopied { realm ->
             EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
                     .isEmpty(EventEntityFields.STATE_KEY)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt
index 5a689378683b141bcd304e9b0cfee2eef70a8a8b..b70e6c1f80f9862d11366df643bca96bbab9e200 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt
@@ -35,7 +35,7 @@ internal class MXOutboundSessionInfo(
         val sessionLifetime = System.currentTimeMillis() - creationTime
 
         if (useCount >= rotationPeriodMsgs || sessionLifetime >= rotationPeriodMs) {
-            Timber.v("## needsRotation() : Rotating megolm session after " + useCount + ", " + sessionLifetime + "ms")
+            Timber.v("## needsRotation() : Rotating megolm session after $useCount, ${sessionLifetime}ms")
             needsRotation = true
         }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt
index 82fb565377764829313f6c24d6d4ee8d71d0725f..96ea5c03fa2540533d33558d08d6e1dafdf55853 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt
@@ -240,6 +240,14 @@ internal interface IMXCryptoStore {
      */
     fun getRoomAlgorithm(roomId: String): String?
 
+    /**
+     * This is a bit different than isRoomEncrypted
+     * A room is encrypted when there is a m.room.encryption state event in the room (malformed/invalid or not)
+     * But the crypto layer has additional guaranty to ensure that encryption would never been reverted
+     * It's defensive coding out of precaution (if ever state is reset)
+     */
+    fun roomWasOnceEncrypted(roomId: String): Boolean
+
     fun shouldEncryptForInvitedMembers(roomId: String): Boolean
 
     fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt
index 33578ba06afa51333205efcbdead4a2845da5c62..a07827c033a94c7c58b927600ca47ef1d45cfa7d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt
@@ -631,7 +631,15 @@ internal class RealmCryptoStore @Inject constructor(
 
     override fun storeRoomAlgorithm(roomId: String, algorithm: String?) {
         doRealmTransaction(realmConfiguration) {
-            CryptoRoomEntity.getOrCreate(it, roomId).algorithm = algorithm
+            CryptoRoomEntity.getOrCreate(it, roomId).let { entity ->
+                entity.algorithm = algorithm
+                // store anyway the new algorithm, but mark the room
+                // as having been encrypted once whatever, this can never
+                // go back to false
+                if (algorithm == MXCRYPTO_ALGORITHM_MEGOLM) {
+                    entity.wasEncryptedOnce = true
+                }
+            }
         }
     }
 
@@ -641,6 +649,12 @@ internal class RealmCryptoStore @Inject constructor(
         }
     }
 
+    override fun roomWasOnceEncrypted(roomId: String): Boolean {
+        return doWithRealm(realmConfiguration) {
+            CryptoRoomEntity.getById(it, roomId)?.wasEncryptedOnce ?: false
+        }
+    }
+
     override fun shouldEncryptForInvitedMembers(roomId: String): Boolean {
         return doWithRealm(realmConfiguration) {
             CryptoRoomEntity.getById(it, roomId)?.shouldEncryptForInvitedMembers
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt
index f73cbaf480aa5e18e090ccd52115eb0b9a83fa1d..cac649948685b08c3ad76d480e38d5c9e3dd106b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt
@@ -16,560 +16,56 @@
 
 package org.matrix.android.sdk.internal.crypto.store.db
 
-import com.squareup.moshi.Moshi
-import com.squareup.moshi.Types
 import io.realm.DynamicRealm
 import io.realm.RealmMigration
-import io.realm.RealmObjectSchema
-import org.matrix.android.sdk.api.extensions.tryOrNull
-import org.matrix.android.sdk.api.util.JsonDict
-import org.matrix.android.sdk.internal.crypto.model.MXDeviceInfo
-import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper
-import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
-import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper
-import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields
-import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntityFields
-import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFields
-import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntityFields
-import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntityFields
-import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields
-import org.matrix.android.sdk.internal.crypto.store.db.model.KeyInfoEntityFields
-import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntityFields
-import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntityFields
-import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields
-import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntityFields
-import org.matrix.android.sdk.internal.crypto.store.db.model.OutboundGroupSessionInfoEntityFields
-import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields
-import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntityFields
-import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntityFields
-import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntityFields
-import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEntityFields
-import org.matrix.android.sdk.internal.di.MoshiProvider
-import org.matrix.android.sdk.internal.di.SerializeNulls
-import org.matrix.androidsdk.crypto.data.MXOlmInboundGroupSession2
+import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo001Legacy
+import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo002Legacy
+import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo003RiotX
+import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo004
+import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo005
+import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo006
+import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo007
+import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo008
+import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo009
+import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo010
+import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo011
+import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo012
+import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo013
+import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo014
+import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo015
 import timber.log.Timber
-import org.matrix.androidsdk.crypto.data.MXDeviceInfo as LegacyMXDeviceInfo
+import javax.inject.Inject
 
-internal object RealmCryptoStoreMigration : RealmMigration {
+internal class RealmCryptoStoreMigration @Inject constructor() : RealmMigration {
+    /**
+     * Forces all RealmCryptoStoreMigration instances to be equal
+     * Avoids Realm throwing when multiple instances of the migration are set
+     */
+    override fun equals(other: Any?) = other is RealmCryptoStoreMigration
+    override fun hashCode() = 5000
 
     // 0, 1, 2: legacy Riot-Android
     // 3: migrate to RiotX schema
     // 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6)
-    const val CRYPTO_STORE_SCHEMA_VERSION = 14L
-
-    private fun RealmObjectSchema.addFieldIfNotExists(fieldName: String, fieldType: Class<*>): RealmObjectSchema {
-        if (!hasField(fieldName)) {
-            addField(fieldName, fieldType)
-        }
-        return this
-    }
-
-    private fun RealmObjectSchema.removeFieldIfExists(fieldName: String): RealmObjectSchema {
-        if (hasField(fieldName)) {
-            removeField(fieldName)
-        }
-        return this
-    }
-
-    private fun RealmObjectSchema.setRequiredIfNotAlready(fieldName: String, isRequired: Boolean): RealmObjectSchema {
-        if (isRequired != isRequired(fieldName)) {
-            setRequired(fieldName, isRequired)
-        }
-        return this
-    }
+    val schemaVersion = 15L
 
     override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
-        Timber.v("Migrating Realm Crypto from $oldVersion to $newVersion")
-
-        if (oldVersion <= 0) migrateTo1Legacy(realm)
-        if (oldVersion <= 1) migrateTo2Legacy(realm)
-        if (oldVersion <= 2) migrateTo3RiotX(realm)
-        if (oldVersion <= 3) migrateTo4(realm)
-        if (oldVersion <= 4) migrateTo5(realm)
-        if (oldVersion <= 5) migrateTo6(realm)
-        if (oldVersion <= 6) migrateTo7(realm)
-        if (oldVersion <= 7) migrateTo8(realm)
-        if (oldVersion <= 8) migrateTo9(realm)
-        if (oldVersion <= 9) migrateTo10(realm)
-        if (oldVersion <= 10) migrateTo11(realm)
-        if (oldVersion <= 11) migrateTo12(realm)
-        if (oldVersion <= 12) migrateTo13(realm)
-        if (oldVersion <= 13) migrateTo14(realm)
-    }
-
-    private fun migrateTo1Legacy(realm: DynamicRealm) {
-        Timber.d("Step 0 -> 1")
-        Timber.d("Add field lastReceivedMessageTs (Long) and set the value to 0")
-
-        realm.schema.get("OlmSessionEntity")
-                ?.addField(OlmSessionEntityFields.LAST_RECEIVED_MESSAGE_TS, Long::class.java)
-                ?.transform {
-                    it.setLong(OlmSessionEntityFields.LAST_RECEIVED_MESSAGE_TS, 0)
-                }
-    }
-
-    private fun migrateTo2Legacy(realm: DynamicRealm) {
-        Timber.d("Step 1 -> 2")
-        Timber.d("Update IncomingRoomKeyRequestEntity format: requestBodyString field is exploded into several fields")
-
-        realm.schema.get("IncomingRoomKeyRequestEntity")
-                ?.addFieldIfNotExists("requestBodyAlgorithm", String::class.java)
-                ?.addFieldIfNotExists("requestBodyRoomId", String::class.java)
-                ?.addFieldIfNotExists("requestBodySenderKey", String::class.java)
-                ?.addFieldIfNotExists("requestBodySessionId", String::class.java)
-                ?.transform { dynamicObject ->
-                    try {
-                        val requestBodyString = dynamicObject.getString("requestBodyString")
-                        // It was a map before
-                        val map: Map<String, String>? = deserializeFromRealm(requestBodyString)
-
-                        map?.let {
-                            dynamicObject.setString("requestBodyAlgorithm", it["algorithm"])
-                            dynamicObject.setString("requestBodyRoomId", it["room_id"])
-                            dynamicObject.setString("requestBodySenderKey", it["sender_key"])
-                            dynamicObject.setString("requestBodySessionId", it["session_id"])
-                        }
-                    } catch (e: Exception) {
-                        Timber.e(e, "Error")
-                    }
-                }
-                ?.removeFieldIfExists("requestBodyString")
-
-        Timber.d("Update IncomingRoomKeyRequestEntity format: requestBodyString field is exploded into several fields")
-
-        realm.schema.get("OutgoingRoomKeyRequestEntity")
-                ?.addFieldIfNotExists("requestBodyAlgorithm", String::class.java)
-                ?.addFieldIfNotExists("requestBodyRoomId", String::class.java)
-                ?.addFieldIfNotExists("requestBodySenderKey", String::class.java)
-                ?.addFieldIfNotExists("requestBodySessionId", String::class.java)
-                ?.transform { dynamicObject ->
-                    try {
-                        val requestBodyString = dynamicObject.getString("requestBodyString")
-                        // It was a map before
-                        val map: Map<String, String>? = deserializeFromRealm(requestBodyString)
-
-                        map?.let {
-                            dynamicObject.setString("requestBodyAlgorithm", it["algorithm"])
-                            dynamicObject.setString("requestBodyRoomId", it["room_id"])
-                            dynamicObject.setString("requestBodySenderKey", it["sender_key"])
-                            dynamicObject.setString("requestBodySessionId", it["session_id"])
-                        }
-                    } catch (e: Exception) {
-                        Timber.e(e, "Error")
-                    }
-                }
-                ?.removeFieldIfExists("requestBodyString")
-
-        Timber.d("Create KeysBackupDataEntity")
-
-        if (!realm.schema.contains("KeysBackupDataEntity")) {
-            realm.schema.create("KeysBackupDataEntity")
-                    .addField(KeysBackupDataEntityFields.PRIMARY_KEY, Integer::class.java)
-                    .addPrimaryKey(KeysBackupDataEntityFields.PRIMARY_KEY)
-                    .setRequired(KeysBackupDataEntityFields.PRIMARY_KEY, true)
-                    .addField(KeysBackupDataEntityFields.BACKUP_LAST_SERVER_HASH, String::class.java)
-                    .addField(KeysBackupDataEntityFields.BACKUP_LAST_SERVER_NUMBER_OF_KEYS, Integer::class.java)
-        }
-    }
-
-    private fun migrateTo3RiotX(realm: DynamicRealm) {
-        Timber.d("Step 2 -> 3")
-        Timber.d("Migrate to RiotX model")
-
-        realm.schema.get("CryptoRoomEntity")
-                ?.addFieldIfNotExists(CryptoRoomEntityFields.SHOULD_ENCRYPT_FOR_INVITED_MEMBERS, Boolean::class.java)
-                ?.setRequiredIfNotAlready(CryptoRoomEntityFields.SHOULD_ENCRYPT_FOR_INVITED_MEMBERS, false)
-
-        // Convert format of MXDeviceInfo, package has to be the same.
-        realm.schema.get("DeviceInfoEntity")
-                ?.transform { obj ->
-                    try {
-                        val oldSerializedData = obj.getString("deviceInfoData")
-                        deserializeFromRealm<LegacyMXDeviceInfo>(oldSerializedData)?.let { legacyMxDeviceInfo ->
-                            val newMxDeviceInfo = MXDeviceInfo(
-                                    deviceId = legacyMxDeviceInfo.deviceId,
-                                    userId = legacyMxDeviceInfo.userId,
-                                    algorithms = legacyMxDeviceInfo.algorithms,
-                                    keys = legacyMxDeviceInfo.keys,
-                                    signatures = legacyMxDeviceInfo.signatures,
-                                    unsigned = legacyMxDeviceInfo.unsigned,
-                                    verified = legacyMxDeviceInfo.mVerified
-                            )
-
-                            obj.setString("deviceInfoData", serializeForRealm(newMxDeviceInfo))
-                        }
-                    } catch (e: Exception) {
-                        Timber.e(e, "Error")
-                    }
-                }
-
-        // Convert MXOlmInboundGroupSession2 to OlmInboundGroupSessionWrapper
-        realm.schema.get("OlmInboundGroupSessionEntity")
-                ?.transform { obj ->
-                    try {
-                        val oldSerializedData = obj.getString("olmInboundGroupSessionData")
-                        deserializeFromRealm<MXOlmInboundGroupSession2>(oldSerializedData)?.let { mxOlmInboundGroupSession2 ->
-                            val sessionKey = mxOlmInboundGroupSession2.mSession.sessionIdentifier()
-                            val newOlmInboundGroupSessionWrapper = OlmInboundGroupSessionWrapper(sessionKey, false)
-                                    .apply {
-                                        olmInboundGroupSession = mxOlmInboundGroupSession2.mSession
-                                        roomId = mxOlmInboundGroupSession2.mRoomId
-                                        senderKey = mxOlmInboundGroupSession2.mSenderKey
-                                        keysClaimed = mxOlmInboundGroupSession2.mKeysClaimed
-                                        forwardingCurve25519KeyChain = mxOlmInboundGroupSession2.mForwardingCurve25519KeyChain
-                                    }
-
-                            obj.setString("olmInboundGroupSessionData", serializeForRealm(newOlmInboundGroupSessionWrapper))
-                        }
-                    } catch (e: Exception) {
-                        Timber.e(e, "Error")
-                    }
-                }
-    }
-
-    // Version 4L added Cross Signing info persistence
-    private fun migrateTo4(realm: DynamicRealm) {
-        Timber.d("Step 3 -> 4")
-
-        if (realm.schema.contains("TrustLevelEntity")) {
-            Timber.d("Skipping Step 3 -> 4 because entities already exist")
-            return
-        }
-
-        Timber.d("Create KeyInfoEntity")
-        val trustLevelEntityEntitySchema = realm.schema.create("TrustLevelEntity")
-                .addField(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, Boolean::class.java)
-                .setNullable(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, true)
-                .addField(TrustLevelEntityFields.LOCALLY_VERIFIED, Boolean::class.java)
-                .setNullable(TrustLevelEntityFields.LOCALLY_VERIFIED, true)
-
-        val keyInfoEntitySchema = realm.schema.create("KeyInfoEntity")
-                .addField(KeyInfoEntityFields.PUBLIC_KEY_BASE64, String::class.java)
-                .addField(KeyInfoEntityFields.SIGNATURES, String::class.java)
-                .addRealmListField(KeyInfoEntityFields.USAGES.`$`, String::class.java)
-                .addRealmObjectField(KeyInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevelEntityEntitySchema)
-
-        Timber.d("Create CrossSigningInfoEntity")
-
-        val crossSigningInfoSchema = realm.schema.create("CrossSigningInfoEntity")
-                .addField(CrossSigningInfoEntityFields.USER_ID, String::class.java)
-                .addPrimaryKey(CrossSigningInfoEntityFields.USER_ID)
-                .addRealmListField(CrossSigningInfoEntityFields.CROSS_SIGNING_KEYS.`$`, keyInfoEntitySchema)
-
-        Timber.d("Updating UserEntity table")
-        realm.schema.get("UserEntity")
-                ?.addRealmObjectField(UserEntityFields.CROSS_SIGNING_INFO_ENTITY.`$`, crossSigningInfoSchema)
-
-        Timber.d("Updating CryptoMetadataEntity table")
-        realm.schema.get("CryptoMetadataEntity")
-                ?.addField(CryptoMetadataEntityFields.X_SIGN_MASTER_PRIVATE_KEY, String::class.java)
-                ?.addField(CryptoMetadataEntityFields.X_SIGN_USER_PRIVATE_KEY, String::class.java)
-                ?.addField(CryptoMetadataEntityFields.X_SIGN_SELF_SIGNED_PRIVATE_KEY, String::class.java)
-
-        val moshi = Moshi.Builder().add(SerializeNulls.JSON_ADAPTER_FACTORY).build()
-        val listMigrationAdapter = moshi.adapter<List<String>>(Types.newParameterizedType(
-                List::class.java,
-                String::class.java,
-                Any::class.java
-        ))
-        val mapMigrationAdapter = moshi.adapter<JsonDict>(Types.newParameterizedType(
-                Map::class.java,
-                String::class.java,
-                Any::class.java
-        ))
-
-        realm.schema.get("DeviceInfoEntity")
-                ?.addField(DeviceInfoEntityFields.USER_ID, String::class.java)
-                ?.addField(DeviceInfoEntityFields.ALGORITHM_LIST_JSON, String::class.java)
-                ?.addField(DeviceInfoEntityFields.KEYS_MAP_JSON, String::class.java)
-                ?.addField(DeviceInfoEntityFields.SIGNATURE_MAP_JSON, String::class.java)
-                ?.addField(DeviceInfoEntityFields.UNSIGNED_MAP_JSON, String::class.java)
-                ?.addField(DeviceInfoEntityFields.IS_BLOCKED, Boolean::class.java)
-                ?.setNullable(DeviceInfoEntityFields.IS_BLOCKED, true)
-                ?.addRealmObjectField(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevelEntityEntitySchema)
-                ?.transform { obj ->
-
-                    try {
-                        val oldSerializedData = obj.getString("deviceInfoData")
-                        deserializeFromRealm<MXDeviceInfo>(oldSerializedData)?.let { oldDevice ->
-
-                            val trustLevel = realm.createObject("TrustLevelEntity")
-                            when (oldDevice.verified) {
-                                MXDeviceInfo.DEVICE_VERIFICATION_UNKNOWN    -> {
-                                    obj.setNull(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`)
-                                }
-                                MXDeviceInfo.DEVICE_VERIFICATION_BLOCKED    -> {
-                                    trustLevel.setNull(TrustLevelEntityFields.LOCALLY_VERIFIED)
-                                    trustLevel.setNull(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED)
-                                    obj.setBoolean(DeviceInfoEntityFields.IS_BLOCKED, oldDevice.isBlocked)
-                                    obj.setObject(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevel)
-                                }
-                                MXDeviceInfo.DEVICE_VERIFICATION_UNVERIFIED -> {
-                                    trustLevel.setBoolean(TrustLevelEntityFields.LOCALLY_VERIFIED, false)
-                                    trustLevel.setBoolean(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, false)
-                                    obj.setObject(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevel)
-                                }
-                                MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED   -> {
-                                    trustLevel.setBoolean(TrustLevelEntityFields.LOCALLY_VERIFIED, true)
-                                    trustLevel.setBoolean(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, false)
-                                    obj.setObject(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevel)
-                                }
-                            }
-
-                            obj.setString(DeviceInfoEntityFields.USER_ID, oldDevice.userId)
-                            obj.setString(DeviceInfoEntityFields.IDENTITY_KEY, oldDevice.identityKey())
-                            obj.setString(DeviceInfoEntityFields.ALGORITHM_LIST_JSON, listMigrationAdapter.toJson(oldDevice.algorithms))
-                            obj.setString(DeviceInfoEntityFields.KEYS_MAP_JSON, mapMigrationAdapter.toJson(oldDevice.keys))
-                            obj.setString(DeviceInfoEntityFields.SIGNATURE_MAP_JSON, mapMigrationAdapter.toJson(oldDevice.signatures))
-                            obj.setString(DeviceInfoEntityFields.UNSIGNED_MAP_JSON, mapMigrationAdapter.toJson(oldDevice.unsigned))
-                        }
-                    } catch (failure: Throwable) {
-                        Timber.w(failure, "Crypto Data base migration error")
-                        // an unfortunate refactor did modify that class, making deserialization failing
-                        // so we just skip and ignore..
-                    }
-                }
-                ?.removeField("deviceInfoData")
-    }
-
-    private fun migrateTo5(realm: DynamicRealm) {
-        Timber.d("Step 4 -> 5")
-        realm.schema.remove("OutgoingRoomKeyRequestEntity")
-        realm.schema.remove("IncomingRoomKeyRequestEntity")
-
-        // Not need to migrate existing request, just start fresh?
-
-        realm.schema.create("GossipingEventEntity")
-                .addField(GossipingEventEntityFields.TYPE, String::class.java)
-                .addIndex(GossipingEventEntityFields.TYPE)
-                .addField(GossipingEventEntityFields.CONTENT, String::class.java)
-                .addField(GossipingEventEntityFields.SENDER, String::class.java)
-                .addIndex(GossipingEventEntityFields.SENDER)
-                .addField(GossipingEventEntityFields.DECRYPTION_RESULT_JSON, String::class.java)
-                .addField(GossipingEventEntityFields.DECRYPTION_ERROR_CODE, String::class.java)
-                .addField(GossipingEventEntityFields.AGE_LOCAL_TS, Long::class.java)
-                .setNullable(GossipingEventEntityFields.AGE_LOCAL_TS, true)
-                .addField(GossipingEventEntityFields.SEND_STATE_STR, String::class.java)
-
-        realm.schema.create("IncomingGossipingRequestEntity")
-                .addField(IncomingGossipingRequestEntityFields.REQUEST_ID, String::class.java)
-                .addIndex(IncomingGossipingRequestEntityFields.REQUEST_ID)
-                .addField(IncomingGossipingRequestEntityFields.TYPE_STR, String::class.java)
-                .addIndex(IncomingGossipingRequestEntityFields.TYPE_STR)
-                .addField(IncomingGossipingRequestEntityFields.OTHER_USER_ID, String::class.java)
-                .addField(IncomingGossipingRequestEntityFields.REQUESTED_INFO_STR, String::class.java)
-                .addField(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, String::class.java)
-                .addField(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, String::class.java)
-                .addField(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, Long::class.java)
-                .setNullable(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, true)
-
-        realm.schema.create("OutgoingGossipingRequestEntity")
-                .addField(OutgoingGossipingRequestEntityFields.REQUEST_ID, String::class.java)
-                .addIndex(OutgoingGossipingRequestEntityFields.REQUEST_ID)
-                .addField(OutgoingGossipingRequestEntityFields.RECIPIENTS_DATA, String::class.java)
-                .addField(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, String::class.java)
-                .addField(OutgoingGossipingRequestEntityFields.TYPE_STR, String::class.java)
-                .addIndex(OutgoingGossipingRequestEntityFields.TYPE_STR)
-                .addField(OutgoingGossipingRequestEntityFields.REQUEST_STATE_STR, String::class.java)
-    }
-
-    private fun migrateTo6(realm: DynamicRealm) {
-        Timber.d("Step 5 -> 6")
-        Timber.d("Updating CryptoMetadataEntity table")
-        realm.schema.get("CryptoMetadataEntity")
-                ?.addField(CryptoMetadataEntityFields.KEY_BACKUP_RECOVERY_KEY, String::class.java)
-                ?.addField(CryptoMetadataEntityFields.KEY_BACKUP_RECOVERY_KEY_VERSION, String::class.java)
-    }
-
-    private fun migrateTo7(realm: DynamicRealm) {
-        Timber.d("Step 6 -> 7")
-        Timber.d("Updating KeyInfoEntity table")
-        val crossSigningKeysMapper = CrossSigningKeysMapper(MoshiProvider.providesMoshi())
-
-        val keyInfoEntities = realm.where("KeyInfoEntity").findAll()
-        try {
-            keyInfoEntities.forEach {
-                val stringSignatures = it.getString(KeyInfoEntityFields.SIGNATURES)
-                val objectSignatures: Map<String, Map<String, String>>? = deserializeFromRealm(stringSignatures)
-                val jsonSignatures = crossSigningKeysMapper.serializeSignatures(objectSignatures)
-                it.setString(KeyInfoEntityFields.SIGNATURES, jsonSignatures)
-            }
-        } catch (failure: Throwable) {
-        }
-
-        // Migrate frozen classes
-        val inboundGroupSessions = realm.where("OlmInboundGroupSessionEntity").findAll()
-        inboundGroupSessions.forEach { dynamicObject ->
-            dynamicObject.getString(OlmInboundGroupSessionEntityFields.OLM_INBOUND_GROUP_SESSION_DATA)?.let { serializedObject ->
-                try {
-                    deserializeFromRealm<OlmInboundGroupSessionWrapper?>(serializedObject)?.let { oldFormat ->
-                        val newFormat = oldFormat.exportKeys()?.let {
-                            OlmInboundGroupSessionWrapper2(it)
-                        }
-                        dynamicObject.setString(OlmInboundGroupSessionEntityFields.OLM_INBOUND_GROUP_SESSION_DATA, serializeForRealm(newFormat))
-                    }
-                } catch (failure: Throwable) {
-                    Timber.e(failure, "## OlmInboundGroupSessionEntity migration failed")
-                }
-            }
-        }
-    }
-
-    private fun migrateTo8(realm: DynamicRealm) {
-        Timber.d("Step 7 -> 8")
-        realm.schema.create("MyDeviceLastSeenInfoEntity")
-                .addField(MyDeviceLastSeenInfoEntityFields.DEVICE_ID, String::class.java)
-                .addPrimaryKey(MyDeviceLastSeenInfoEntityFields.DEVICE_ID)
-                .addField(MyDeviceLastSeenInfoEntityFields.DISPLAY_NAME, String::class.java)
-                .addField(MyDeviceLastSeenInfoEntityFields.LAST_SEEN_IP, String::class.java)
-                .addField(MyDeviceLastSeenInfoEntityFields.LAST_SEEN_TS, Long::class.java)
-                .setNullable(MyDeviceLastSeenInfoEntityFields.LAST_SEEN_TS, true)
-
-        val now = System.currentTimeMillis()
-        realm.schema.get("DeviceInfoEntity")
-                ?.addField(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, Long::class.java)
-                ?.setNullable(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, true)
-                ?.transform { deviceInfoEntity ->
-                    tryOrNull {
-                        deviceInfoEntity.setLong(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, now)
-                    }
-                }
-    }
-
-    // Fixes duplicate devices in UserEntity#devices
-    private fun migrateTo9(realm: DynamicRealm) {
-        Timber.d("Step 8 -> 9")
-        val userEntities = realm.where("UserEntity").findAll()
-        userEntities.forEach {
-            try {
-                val deviceList = it.getList(UserEntityFields.DEVICES.`$`)
-                        ?: return@forEach
-                val distinct = deviceList.distinctBy { it.getString(DeviceInfoEntityFields.DEVICE_ID) }
-                if (distinct.size != deviceList.size) {
-                    deviceList.clear()
-                    deviceList.addAll(distinct)
-                }
-            } catch (failure: Throwable) {
-                Timber.w(failure, "Crypto Data base migration error for migrateTo9")
-            }
-        }
-    }
-
-    // Version 10L added WithHeld Keys Info (MSC2399)
-    private fun migrateTo10(realm: DynamicRealm) {
-        Timber.d("Step 9 -> 10")
-        realm.schema.create("WithHeldSessionEntity")
-                .addField(WithHeldSessionEntityFields.ROOM_ID, String::class.java)
-                .addField(WithHeldSessionEntityFields.ALGORITHM, String::class.java)
-                .addField(WithHeldSessionEntityFields.SESSION_ID, String::class.java)
-                .addIndex(WithHeldSessionEntityFields.SESSION_ID)
-                .addField(WithHeldSessionEntityFields.SENDER_KEY, String::class.java)
-                .addIndex(WithHeldSessionEntityFields.SENDER_KEY)
-                .addField(WithHeldSessionEntityFields.CODE_STRING, String::class.java)
-                .addField(WithHeldSessionEntityFields.REASON, String::class.java)
-
-        realm.schema.create("SharedSessionEntity")
-                .addField(SharedSessionEntityFields.ROOM_ID, String::class.java)
-                .addField(SharedSessionEntityFields.ALGORITHM, String::class.java)
-                .addField(SharedSessionEntityFields.SESSION_ID, String::class.java)
-                .addIndex(SharedSessionEntityFields.SESSION_ID)
-                .addField(SharedSessionEntityFields.USER_ID, String::class.java)
-                .addIndex(SharedSessionEntityFields.USER_ID)
-                .addField(SharedSessionEntityFields.DEVICE_ID, String::class.java)
-                .addIndex(SharedSessionEntityFields.DEVICE_ID)
-                .addField(SharedSessionEntityFields.CHAIN_INDEX, Long::class.java)
-                .setNullable(SharedSessionEntityFields.CHAIN_INDEX, true)
-    }
-
-    // Version 11L added deviceKeysSentToServer boolean to CryptoMetadataEntity
-    private fun migrateTo11(realm: DynamicRealm) {
-        Timber.d("Step 10 -> 11")
-        realm.schema.get("CryptoMetadataEntity")
-                ?.addField(CryptoMetadataEntityFields.DEVICE_KEYS_SENT_TO_SERVER, Boolean::class.java)
-    }
-
-    // Version 12L added outbound group session persistence
-    private fun migrateTo12(realm: DynamicRealm) {
-        Timber.d("Step 11 -> 12")
-        val outboundEntitySchema = realm.schema.create("OutboundGroupSessionInfoEntity")
-                .addField(OutboundGroupSessionInfoEntityFields.SERIALIZED_OUTBOUND_SESSION_DATA, String::class.java)
-                .addField(OutboundGroupSessionInfoEntityFields.CREATION_TIME, Long::class.java)
-                .setNullable(OutboundGroupSessionInfoEntityFields.CREATION_TIME, true)
-
-        realm.schema.get("CryptoRoomEntity")
-                ?.addRealmObjectField(CryptoRoomEntityFields.OUTBOUND_SESSION_INFO.`$`, outboundEntitySchema)
-    }
-
-    // Version 13L delete unreferenced TrustLevelEntity
-    private fun migrateTo13(realm: DynamicRealm) {
-        Timber.d("Step 12 -> 13")
-
-        // Use a trick to do that... Ref: https://stackoverflow.com/questions/55221366
-        val trustLevelEntitySchema = realm.schema.get("TrustLevelEntity")
-
-        /*
-        Creating a new temp field called isLinked which is set to true for those which are
-        references by other objects. Rest of them are set to false. Then removing all
-        those which are false and hence duplicate and unnecessary. Then removing the temp field
-        isLinked
-         */
-        var mainCounter = 0
-        var deviceInfoCounter = 0
-        var keyInfoCounter = 0
-        val deleteCounter: Int
-
-        trustLevelEntitySchema
-                ?.addField("isLinked", Boolean::class.java)
-                ?.transform { obj ->
-                    // Setting to false for all by default
-                    obj.set("isLinked", false)
-                    mainCounter++
-                }
-
-        realm.schema.get("DeviceInfoEntity")?.transform { obj ->
-            // Setting to true for those which are referenced in DeviceInfoEntity
-            deviceInfoCounter++
-            obj.getObject("trustLevelEntity")?.set("isLinked", true)
-        }
-
-        realm.schema.get("KeyInfoEntity")?.transform { obj ->
-            // Setting to true for those which are referenced in KeyInfoEntity
-            keyInfoCounter++
-            obj.getObject("trustLevelEntity")?.set("isLinked", true)
-        }
-
-        // Removing all those which are set as false
-        realm.where("TrustLevelEntity")
-                .equalTo("isLinked", false)
-                .findAll()
-                .also { deleteCounter = it.size }
-                .deleteAllFromRealm()
-
-        trustLevelEntitySchema?.removeField("isLinked")
-
-        Timber.w("TrustLevelEntity cleanup: $mainCounter entities")
-        Timber.w("TrustLevelEntity cleanup: $deviceInfoCounter entities referenced in DeviceInfoEntities")
-        Timber.w("TrustLevelEntity cleanup: $keyInfoCounter entities referenced in KeyInfoEntity")
-        Timber.w("TrustLevelEntity cleanup: $deleteCounter entities deleted!")
-        if (mainCounter != deviceInfoCounter + keyInfoCounter + deleteCounter) {
-            Timber.e("TrustLevelEntity cleanup: Something is not correct...")
-        }
-    }
-
-    // Version 14L Update the way we remember key sharing
-    private fun migrateTo14(realm: DynamicRealm) {
-        Timber.d("Step 13 -> 14")
-        realm.schema.get("SharedSessionEntity")
-                ?.addField(SharedSessionEntityFields.DEVICE_IDENTITY_KEY, String::class.java)
-                ?.addIndex(SharedSessionEntityFields.DEVICE_IDENTITY_KEY)
-                ?.transform {
-                    val sharedUserId = it.getString(SharedSessionEntityFields.USER_ID)
-                    val sharedDeviceId = it.getString(SharedSessionEntityFields.DEVICE_ID)
-                    val knownDevice = realm.where("DeviceInfoEntity")
-                            .equalTo(DeviceInfoEntityFields.USER_ID, sharedUserId)
-                            .equalTo(DeviceInfoEntityFields.DEVICE_ID, sharedDeviceId)
-                            .findFirst()
-                    it.setString(SharedSessionEntityFields.DEVICE_IDENTITY_KEY, knownDevice?.getString(DeviceInfoEntityFields.IDENTITY_KEY))
-                }
+        Timber.d("Migrating Realm Crypto from $oldVersion to $newVersion")
+
+        if (oldVersion < 1) MigrateCryptoTo001Legacy(realm).perform()
+        if (oldVersion < 2) MigrateCryptoTo002Legacy(realm).perform()
+        if (oldVersion < 3) MigrateCryptoTo003RiotX(realm).perform()
+        if (oldVersion < 4) MigrateCryptoTo004(realm).perform()
+        if (oldVersion < 5) MigrateCryptoTo005(realm).perform()
+        if (oldVersion < 6) MigrateCryptoTo006(realm).perform()
+        if (oldVersion < 7) MigrateCryptoTo007(realm).perform()
+        if (oldVersion < 8) MigrateCryptoTo008(realm).perform()
+        if (oldVersion < 9) MigrateCryptoTo009(realm).perform()
+        if (oldVersion < 10) MigrateCryptoTo010(realm).perform()
+        if (oldVersion < 11) MigrateCryptoTo011(realm).perform()
+        if (oldVersion < 12) MigrateCryptoTo012(realm).perform()
+        if (oldVersion < 13) MigrateCryptoTo013(realm).perform()
+        if (oldVersion < 14) MigrateCryptoTo014(realm).perform()
+        if (oldVersion < 15) MigrateCryptoTo015(realm).perform()
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo001Legacy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo001Legacy.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0e44689428ab896e1804aefcfe468408ab10ea50
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo001Legacy.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.crypto.store.db.migration
+
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+import timber.log.Timber
+
+class MigrateCryptoTo001Legacy(realm: DynamicRealm) : RealmMigrator(realm, 1) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        Timber.d("Add field lastReceivedMessageTs (Long) and set the value to 0")
+
+        realm.schema.get("OlmSessionEntity")
+                ?.addField(OlmSessionEntityFields.LAST_RECEIVED_MESSAGE_TS, Long::class.java)
+                ?.transform {
+                    it.setLong(OlmSessionEntityFields.LAST_RECEIVED_MESSAGE_TS, 0)
+                }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo002Legacy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo002Legacy.kt
new file mode 100644
index 0000000000000000000000000000000000000000..84e627a688e987a9772beca7b84df260a5d36db2
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo002Legacy.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.crypto.store.db.migration
+
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm
+import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+import timber.log.Timber
+
+class MigrateCryptoTo002Legacy(realm: DynamicRealm) : RealmMigrator(realm, 2) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        Timber.d("Update IncomingRoomKeyRequestEntity format: requestBodyString field is exploded into several fields")
+        realm.schema.get("IncomingRoomKeyRequestEntity")
+                ?.addFieldIfNotExists("requestBodyAlgorithm", String::class.java)
+                ?.addFieldIfNotExists("requestBodyRoomId", String::class.java)
+                ?.addFieldIfNotExists("requestBodySenderKey", String::class.java)
+                ?.addFieldIfNotExists("requestBodySessionId", String::class.java)
+                ?.transform { dynamicObject ->
+                    try {
+                        val requestBodyString = dynamicObject.getString("requestBodyString")
+                        // It was a map before
+                        val map: Map<String, String>? = deserializeFromRealm(requestBodyString)
+
+                        map?.let {
+                            dynamicObject.setString("requestBodyAlgorithm", it["algorithm"])
+                            dynamicObject.setString("requestBodyRoomId", it["room_id"])
+                            dynamicObject.setString("requestBodySenderKey", it["sender_key"])
+                            dynamicObject.setString("requestBodySessionId", it["session_id"])
+                        }
+                    } catch (e: Exception) {
+                        Timber.e(e, "Error")
+                    }
+                }
+                ?.removeFieldIfExists("requestBodyString")
+
+        Timber.d("Update IncomingRoomKeyRequestEntity format: requestBodyString field is exploded into several fields")
+        realm.schema.get("OutgoingRoomKeyRequestEntity")
+                ?.addFieldIfNotExists("requestBodyAlgorithm", String::class.java)
+                ?.addFieldIfNotExists("requestBodyRoomId", String::class.java)
+                ?.addFieldIfNotExists("requestBodySenderKey", String::class.java)
+                ?.addFieldIfNotExists("requestBodySessionId", String::class.java)
+                ?.transform { dynamicObject ->
+                    try {
+                        val requestBodyString = dynamicObject.getString("requestBodyString")
+                        // It was a map before
+                        val map: Map<String, String>? = deserializeFromRealm(requestBodyString)
+
+                        map?.let {
+                            dynamicObject.setString("requestBodyAlgorithm", it["algorithm"])
+                            dynamicObject.setString("requestBodyRoomId", it["room_id"])
+                            dynamicObject.setString("requestBodySenderKey", it["sender_key"])
+                            dynamicObject.setString("requestBodySessionId", it["session_id"])
+                        }
+                    } catch (e: Exception) {
+                        Timber.e(e, "Error")
+                    }
+                }
+                ?.removeFieldIfExists("requestBodyString")
+
+        Timber.d("Create KeysBackupDataEntity")
+        if (!realm.schema.contains("KeysBackupDataEntity")) {
+            realm.schema.create("KeysBackupDataEntity")
+                    .addField(KeysBackupDataEntityFields.PRIMARY_KEY, Integer::class.java)
+                    .addPrimaryKey(KeysBackupDataEntityFields.PRIMARY_KEY)
+                    .setRequired(KeysBackupDataEntityFields.PRIMARY_KEY, true)
+                    .addField(KeysBackupDataEntityFields.BACKUP_LAST_SERVER_HASH, String::class.java)
+                    .addField(KeysBackupDataEntityFields.BACKUP_LAST_SERVER_NUMBER_OF_KEYS, Integer::class.java)
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo003RiotX.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo003RiotX.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b468a56af6e39751d7a77b2cb280ccf6161a65d5
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo003RiotX.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.crypto.store.db.migration
+
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper
+import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm
+import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFields
+import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+import org.matrix.androidsdk.crypto.data.MXDeviceInfo
+import org.matrix.androidsdk.crypto.data.MXOlmInboundGroupSession2
+import timber.log.Timber
+
+class MigrateCryptoTo003RiotX(realm: DynamicRealm) : RealmMigrator(realm, 3) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        Timber.d("Migrate to RiotX model")
+        realm.schema.get("CryptoRoomEntity")
+                ?.addFieldIfNotExists(CryptoRoomEntityFields.SHOULD_ENCRYPT_FOR_INVITED_MEMBERS, Boolean::class.java)
+                ?.setRequiredIfNotAlready(CryptoRoomEntityFields.SHOULD_ENCRYPT_FOR_INVITED_MEMBERS, false)
+
+        // Convert format of MXDeviceInfo, package has to be the same.
+        realm.schema.get("DeviceInfoEntity")
+                ?.transform { obj ->
+                    try {
+                        val oldSerializedData = obj.getString("deviceInfoData")
+                        deserializeFromRealm<MXDeviceInfo>(oldSerializedData)?.let { legacyMxDeviceInfo ->
+                            val newMxDeviceInfo = org.matrix.android.sdk.internal.crypto.model.MXDeviceInfo(
+                                    deviceId = legacyMxDeviceInfo.deviceId,
+                                    userId = legacyMxDeviceInfo.userId,
+                                    algorithms = legacyMxDeviceInfo.algorithms,
+                                    keys = legacyMxDeviceInfo.keys,
+                                    signatures = legacyMxDeviceInfo.signatures,
+                                    unsigned = legacyMxDeviceInfo.unsigned,
+                                    verified = legacyMxDeviceInfo.mVerified
+                            )
+
+                            obj.setString("deviceInfoData", serializeForRealm(newMxDeviceInfo))
+                        }
+                    } catch (e: Exception) {
+                        Timber.e(e, "Error")
+                    }
+                }
+
+        // Convert MXOlmInboundGroupSession2 to OlmInboundGroupSessionWrapper
+        realm.schema.get("OlmInboundGroupSessionEntity")
+                ?.transform { obj ->
+                    try {
+                        val oldSerializedData = obj.getString("olmInboundGroupSessionData")
+                        deserializeFromRealm<MXOlmInboundGroupSession2>(oldSerializedData)?.let { mxOlmInboundGroupSession2 ->
+                            val sessionKey = mxOlmInboundGroupSession2.mSession.sessionIdentifier()
+                            val newOlmInboundGroupSessionWrapper = OlmInboundGroupSessionWrapper(sessionKey, false)
+                                    .apply {
+                                        olmInboundGroupSession = mxOlmInboundGroupSession2.mSession
+                                        roomId = mxOlmInboundGroupSession2.mRoomId
+                                        senderKey = mxOlmInboundGroupSession2.mSenderKey
+                                        keysClaimed = mxOlmInboundGroupSession2.mKeysClaimed
+                                        forwardingCurve25519KeyChain = mxOlmInboundGroupSession2.mForwardingCurve25519KeyChain
+                                    }
+
+                            obj.setString("olmInboundGroupSessionData", serializeForRealm(newOlmInboundGroupSessionWrapper))
+                        }
+                    } catch (e: Exception) {
+                        Timber.e(e, "Error")
+                    }
+                }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo004.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo004.kt
new file mode 100644
index 0000000000000000000000000000000000000000..20a4814b8d1d69e1ced0cfa578e1f3eb05f65dbf
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo004.kt
@@ -0,0 +1,139 @@
+/*
+ * 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.crypto.store.db.migration
+
+import com.squareup.moshi.Moshi
+import com.squareup.moshi.Types
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.api.util.JsonDict
+import org.matrix.android.sdk.internal.crypto.model.MXDeviceInfo
+import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm
+import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields
+import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntityFields
+import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntityFields
+import org.matrix.android.sdk.internal.crypto.store.db.model.KeyInfoEntityFields
+import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntityFields
+import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntityFields
+import org.matrix.android.sdk.internal.di.SerializeNulls
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+import timber.log.Timber
+
+// Version 4L added Cross Signing info persistence
+class MigrateCryptoTo004(realm: DynamicRealm) : RealmMigrator(realm, 4) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        if (realm.schema.contains("TrustLevelEntity")) {
+            Timber.d("Skipping Step 3 -> 4 because entities already exist")
+            return
+        }
+
+        Timber.d("Create KeyInfoEntity")
+        val trustLevelEntityEntitySchema = realm.schema.create("TrustLevelEntity")
+                .addField(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, Boolean::class.java)
+                .setNullable(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, true)
+                .addField(TrustLevelEntityFields.LOCALLY_VERIFIED, Boolean::class.java)
+                .setNullable(TrustLevelEntityFields.LOCALLY_VERIFIED, true)
+
+        val keyInfoEntitySchema = realm.schema.create("KeyInfoEntity")
+                .addField(KeyInfoEntityFields.PUBLIC_KEY_BASE64, String::class.java)
+                .addField(KeyInfoEntityFields.SIGNATURES, String::class.java)
+                .addRealmListField(KeyInfoEntityFields.USAGES.`$`, String::class.java)
+                .addRealmObjectField(KeyInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevelEntityEntitySchema)
+
+        Timber.d("Create CrossSigningInfoEntity")
+
+        val crossSigningInfoSchema = realm.schema.create("CrossSigningInfoEntity")
+                .addField(CrossSigningInfoEntityFields.USER_ID, String::class.java)
+                .addPrimaryKey(CrossSigningInfoEntityFields.USER_ID)
+                .addRealmListField(CrossSigningInfoEntityFields.CROSS_SIGNING_KEYS.`$`, keyInfoEntitySchema)
+
+        Timber.d("Updating UserEntity table")
+        realm.schema.get("UserEntity")
+                ?.addRealmObjectField(UserEntityFields.CROSS_SIGNING_INFO_ENTITY.`$`, crossSigningInfoSchema)
+
+        Timber.d("Updating CryptoMetadataEntity table")
+        realm.schema.get("CryptoMetadataEntity")
+                ?.addField(CryptoMetadataEntityFields.X_SIGN_MASTER_PRIVATE_KEY, String::class.java)
+                ?.addField(CryptoMetadataEntityFields.X_SIGN_USER_PRIVATE_KEY, String::class.java)
+                ?.addField(CryptoMetadataEntityFields.X_SIGN_SELF_SIGNED_PRIVATE_KEY, String::class.java)
+
+        val moshi = Moshi.Builder().add(SerializeNulls.JSON_ADAPTER_FACTORY).build()
+        val listMigrationAdapter = moshi.adapter<List<String>>(Types.newParameterizedType(
+                List::class.java,
+                String::class.java,
+                Any::class.java
+        ))
+        val mapMigrationAdapter = moshi.adapter<JsonDict>(Types.newParameterizedType(
+                Map::class.java,
+                String::class.java,
+                Any::class.java
+        ))
+
+        realm.schema.get("DeviceInfoEntity")
+                ?.addField(DeviceInfoEntityFields.USER_ID, String::class.java)
+                ?.addField(DeviceInfoEntityFields.ALGORITHM_LIST_JSON, String::class.java)
+                ?.addField(DeviceInfoEntityFields.KEYS_MAP_JSON, String::class.java)
+                ?.addField(DeviceInfoEntityFields.SIGNATURE_MAP_JSON, String::class.java)
+                ?.addField(DeviceInfoEntityFields.UNSIGNED_MAP_JSON, String::class.java)
+                ?.addField(DeviceInfoEntityFields.IS_BLOCKED, Boolean::class.java)
+                ?.setNullable(DeviceInfoEntityFields.IS_BLOCKED, true)
+                ?.addRealmObjectField(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevelEntityEntitySchema)
+                ?.transform { obj ->
+
+                    try {
+                        val oldSerializedData = obj.getString("deviceInfoData")
+                        deserializeFromRealm<MXDeviceInfo>(oldSerializedData)?.let { oldDevice ->
+
+                            val trustLevel = realm.createObject("TrustLevelEntity")
+                            when (oldDevice.verified) {
+                                MXDeviceInfo.DEVICE_VERIFICATION_UNKNOWN    -> {
+                                    obj.setNull(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`)
+                                }
+                                MXDeviceInfo.DEVICE_VERIFICATION_BLOCKED    -> {
+                                    trustLevel.setNull(TrustLevelEntityFields.LOCALLY_VERIFIED)
+                                    trustLevel.setNull(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED)
+                                    obj.setBoolean(DeviceInfoEntityFields.IS_BLOCKED, oldDevice.isBlocked)
+                                    obj.setObject(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevel)
+                                }
+                                MXDeviceInfo.DEVICE_VERIFICATION_UNVERIFIED -> {
+                                    trustLevel.setBoolean(TrustLevelEntityFields.LOCALLY_VERIFIED, false)
+                                    trustLevel.setBoolean(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, false)
+                                    obj.setObject(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevel)
+                                }
+                                MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED   -> {
+                                    trustLevel.setBoolean(TrustLevelEntityFields.LOCALLY_VERIFIED, true)
+                                    trustLevel.setBoolean(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, false)
+                                    obj.setObject(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevel)
+                                }
+                            }
+
+                            obj.setString(DeviceInfoEntityFields.USER_ID, oldDevice.userId)
+                            obj.setString(DeviceInfoEntityFields.IDENTITY_KEY, oldDevice.identityKey())
+                            obj.setString(DeviceInfoEntityFields.ALGORITHM_LIST_JSON, listMigrationAdapter.toJson(oldDevice.algorithms))
+                            obj.setString(DeviceInfoEntityFields.KEYS_MAP_JSON, mapMigrationAdapter.toJson(oldDevice.keys))
+                            obj.setString(DeviceInfoEntityFields.SIGNATURE_MAP_JSON, mapMigrationAdapter.toJson(oldDevice.signatures))
+                            obj.setString(DeviceInfoEntityFields.UNSIGNED_MAP_JSON, mapMigrationAdapter.toJson(oldDevice.unsigned))
+                        }
+                    } catch (failure: Throwable) {
+                        Timber.w(failure, "Crypto Data base migration error")
+                        // an unfortunate refactor did modify that class, making deserialization failing
+                        // so we just skip and ignore..
+                    }
+                }
+                ?.removeField("deviceInfoData")
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo005.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo005.kt
new file mode 100644
index 0000000000000000000000000000000000000000..8365d3446440828b53fb90e84d830e5b382602b1
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo005.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.crypto.store.db.migration
+
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntityFields
+import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields
+import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+class MigrateCryptoTo005(realm: DynamicRealm) : RealmMigrator(realm, 5) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.remove("OutgoingRoomKeyRequestEntity")
+        realm.schema.remove("IncomingRoomKeyRequestEntity")
+
+        // Not need to migrate existing request, just start fresh?
+
+        realm.schema.create("GossipingEventEntity")
+                .addField(GossipingEventEntityFields.TYPE, String::class.java)
+                .addIndex(GossipingEventEntityFields.TYPE)
+                .addField(GossipingEventEntityFields.CONTENT, String::class.java)
+                .addField(GossipingEventEntityFields.SENDER, String::class.java)
+                .addIndex(GossipingEventEntityFields.SENDER)
+                .addField(GossipingEventEntityFields.DECRYPTION_RESULT_JSON, String::class.java)
+                .addField(GossipingEventEntityFields.DECRYPTION_ERROR_CODE, String::class.java)
+                .addField(GossipingEventEntityFields.AGE_LOCAL_TS, Long::class.java)
+                .setNullable(GossipingEventEntityFields.AGE_LOCAL_TS, true)
+                .addField(GossipingEventEntityFields.SEND_STATE_STR, String::class.java)
+
+        realm.schema.create("IncomingGossipingRequestEntity")
+                .addField(IncomingGossipingRequestEntityFields.REQUEST_ID, String::class.java)
+                .addIndex(IncomingGossipingRequestEntityFields.REQUEST_ID)
+                .addField(IncomingGossipingRequestEntityFields.TYPE_STR, String::class.java)
+                .addIndex(IncomingGossipingRequestEntityFields.TYPE_STR)
+                .addField(IncomingGossipingRequestEntityFields.OTHER_USER_ID, String::class.java)
+                .addField(IncomingGossipingRequestEntityFields.REQUESTED_INFO_STR, String::class.java)
+                .addField(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, String::class.java)
+                .addField(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, String::class.java)
+                .addField(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, Long::class.java)
+                .setNullable(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, true)
+
+        realm.schema.create("OutgoingGossipingRequestEntity")
+                .addField(OutgoingGossipingRequestEntityFields.REQUEST_ID, String::class.java)
+                .addIndex(OutgoingGossipingRequestEntityFields.REQUEST_ID)
+                .addField(OutgoingGossipingRequestEntityFields.RECIPIENTS_DATA, String::class.java)
+                .addField(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, String::class.java)
+                .addField(OutgoingGossipingRequestEntityFields.TYPE_STR, String::class.java)
+                .addIndex(OutgoingGossipingRequestEntityFields.TYPE_STR)
+                .addField(OutgoingGossipingRequestEntityFields.REQUEST_STATE_STR, String::class.java)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo006.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo006.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a29a7918268e062532c4de0bccaba060f82cad8f
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo006.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.crypto.store.db.migration
+
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+import timber.log.Timber
+
+class MigrateCryptoTo006(realm: DynamicRealm) : RealmMigrator(realm, 6) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        Timber.d("Updating CryptoMetadataEntity table")
+        realm.schema.get("CryptoMetadataEntity")
+                ?.addField(CryptoMetadataEntityFields.KEY_BACKUP_RECOVERY_KEY, String::class.java)
+                ?.addField(CryptoMetadataEntityFields.KEY_BACKUP_RECOVERY_KEY_VERSION, String::class.java)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo007.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo007.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7ae58e7fc052082c88bfcc291209a2b2c11da028
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo007.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.crypto.store.db.migration
+
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper
+import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
+import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm
+import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper
+import org.matrix.android.sdk.internal.crypto.store.db.model.KeyInfoEntityFields
+import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields
+import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm
+import org.matrix.android.sdk.internal.di.MoshiProvider
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+import timber.log.Timber
+
+class MigrateCryptoTo007(realm: DynamicRealm) : RealmMigrator(realm, 7) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        Timber.d("Updating KeyInfoEntity table")
+        val crossSigningKeysMapper = CrossSigningKeysMapper(MoshiProvider.providesMoshi())
+
+        val keyInfoEntities = realm.where("KeyInfoEntity").findAll()
+        try {
+            keyInfoEntities.forEach {
+                val stringSignatures = it.getString(KeyInfoEntityFields.SIGNATURES)
+                val objectSignatures: Map<String, Map<String, String>>? = deserializeFromRealm(stringSignatures)
+                val jsonSignatures = crossSigningKeysMapper.serializeSignatures(objectSignatures)
+                it.setString(KeyInfoEntityFields.SIGNATURES, jsonSignatures)
+            }
+        } catch (failure: Throwable) {
+        }
+
+        // Migrate frozen classes
+        val inboundGroupSessions = realm.where("OlmInboundGroupSessionEntity").findAll()
+        inboundGroupSessions.forEach { dynamicObject ->
+            dynamicObject.getString(OlmInboundGroupSessionEntityFields.OLM_INBOUND_GROUP_SESSION_DATA)?.let { serializedObject ->
+                try {
+                    deserializeFromRealm<OlmInboundGroupSessionWrapper?>(serializedObject)?.let { oldFormat ->
+                        val newFormat = oldFormat.exportKeys()?.let {
+                            OlmInboundGroupSessionWrapper2(it)
+                        }
+                        dynamicObject.setString(OlmInboundGroupSessionEntityFields.OLM_INBOUND_GROUP_SESSION_DATA, serializeForRealm(newFormat))
+                    }
+                } catch (failure: Throwable) {
+                    Timber.e(failure, "## OlmInboundGroupSessionEntity migration failed")
+                }
+            }
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo008.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo008.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e3bd3f035a290bf076b1ea69de4edc59406de053
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo008.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.crypto.store.db.migration
+
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntityFields
+import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+class MigrateCryptoTo008(realm: DynamicRealm) : RealmMigrator(realm, 8) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.create("MyDeviceLastSeenInfoEntity")
+                .addField(MyDeviceLastSeenInfoEntityFields.DEVICE_ID, String::class.java)
+                .addPrimaryKey(MyDeviceLastSeenInfoEntityFields.DEVICE_ID)
+                .addField(MyDeviceLastSeenInfoEntityFields.DISPLAY_NAME, String::class.java)
+                .addField(MyDeviceLastSeenInfoEntityFields.LAST_SEEN_IP, String::class.java)
+                .addField(MyDeviceLastSeenInfoEntityFields.LAST_SEEN_TS, Long::class.java)
+                .setNullable(MyDeviceLastSeenInfoEntityFields.LAST_SEEN_TS, true)
+
+        val now = System.currentTimeMillis()
+        realm.schema.get("DeviceInfoEntity")
+                ?.addField(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, Long::class.java)
+                ?.setNullable(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, true)
+                ?.transform { deviceInfoEntity ->
+                    tryOrNull {
+                        deviceInfoEntity.setLong(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, now)
+                    }
+                }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo009.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo009.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ed705318f9e0637f15d9d56b226e8f4faf8d1f45
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo009.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.crypto.store.db.migration
+
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntityFields
+import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+import timber.log.Timber
+
+// Fixes duplicate devices in UserEntity#devices
+class MigrateCryptoTo009(realm: DynamicRealm) : RealmMigrator(realm, 9) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        val userEntities = realm.where("UserEntity").findAll()
+        userEntities.forEach {
+            try {
+                val deviceList = it.getList(UserEntityFields.DEVICES.`$`)
+                        ?: return@forEach
+                val distinct = deviceList.distinctBy { it.getString(DeviceInfoEntityFields.DEVICE_ID) }
+                if (distinct.size != deviceList.size) {
+                    deviceList.clear()
+                    deviceList.addAll(distinct)
+                }
+            } catch (failure: Throwable) {
+                Timber.w(failure, "Crypto Data base migration error for migrateTo9")
+            }
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo010.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo010.kt
new file mode 100644
index 0000000000000000000000000000000000000000..8d69ee5558890013b6fd786f5f0e5a7c401c1f94
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo010.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.crypto.store.db.migration
+
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntityFields
+import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+// Version 10L added WithHeld Keys Info (MSC2399)
+class MigrateCryptoTo010(realm: DynamicRealm) : RealmMigrator(realm, 10) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.create("WithHeldSessionEntity")
+                .addField(WithHeldSessionEntityFields.ROOM_ID, String::class.java)
+                .addField(WithHeldSessionEntityFields.ALGORITHM, String::class.java)
+                .addField(WithHeldSessionEntityFields.SESSION_ID, String::class.java)
+                .addIndex(WithHeldSessionEntityFields.SESSION_ID)
+                .addField(WithHeldSessionEntityFields.SENDER_KEY, String::class.java)
+                .addIndex(WithHeldSessionEntityFields.SENDER_KEY)
+                .addField(WithHeldSessionEntityFields.CODE_STRING, String::class.java)
+                .addField(WithHeldSessionEntityFields.REASON, String::class.java)
+
+        realm.schema.create("SharedSessionEntity")
+                .addField(SharedSessionEntityFields.ROOM_ID, String::class.java)
+                .addField(SharedSessionEntityFields.ALGORITHM, String::class.java)
+                .addField(SharedSessionEntityFields.SESSION_ID, String::class.java)
+                .addIndex(SharedSessionEntityFields.SESSION_ID)
+                .addField(SharedSessionEntityFields.USER_ID, String::class.java)
+                .addIndex(SharedSessionEntityFields.USER_ID)
+                .addField(SharedSessionEntityFields.DEVICE_ID, String::class.java)
+                .addIndex(SharedSessionEntityFields.DEVICE_ID)
+                .addField(SharedSessionEntityFields.CHAIN_INDEX, Long::class.java)
+                .setNullable(SharedSessionEntityFields.CHAIN_INDEX, true)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo011.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo011.kt
new file mode 100644
index 0000000000000000000000000000000000000000..c9825a7f3d44042e365972c5775be9b72e9948de
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo011.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.crypto.store.db.migration
+
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+// Version 11L added deviceKeysSentToServer boolean to CryptoMetadataEntity
+class MigrateCryptoTo011(realm: DynamicRealm) : RealmMigrator(realm, 11) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.get("CryptoMetadataEntity")
+                ?.addField(CryptoMetadataEntityFields.DEVICE_KEYS_SENT_TO_SERVER, Boolean::class.java)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo012.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo012.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6b1460d9d6c7c14e3f36de9e01c1f434f5abb943
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo012.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.crypto.store.db.migration
+
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFields
+import org.matrix.android.sdk.internal.crypto.store.db.model.OutboundGroupSessionInfoEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+// Version 12L added outbound group session persistence
+class MigrateCryptoTo012(realm: DynamicRealm) : RealmMigrator(realm, 12) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        val outboundEntitySchema = realm.schema.create("OutboundGroupSessionInfoEntity")
+                .addField(OutboundGroupSessionInfoEntityFields.SERIALIZED_OUTBOUND_SESSION_DATA, String::class.java)
+                .addField(OutboundGroupSessionInfoEntityFields.CREATION_TIME, Long::class.java)
+                .setNullable(OutboundGroupSessionInfoEntityFields.CREATION_TIME, true)
+
+        realm.schema.get("CryptoRoomEntity")
+                ?.addRealmObjectField(CryptoRoomEntityFields.OUTBOUND_SESSION_INFO.`$`, outboundEntitySchema)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo013.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo013.kt
new file mode 100644
index 0000000000000000000000000000000000000000..dc22c5f133d6f80d83cd4ecbb5f1ccea9359ab92
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo013.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.crypto.store.db.migration
+
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+import timber.log.Timber
+
+// Version 13L delete unreferenced TrustLevelEntity
+class MigrateCryptoTo013(realm: DynamicRealm) : RealmMigrator(realm, 13) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        // Use a trick to do that... Ref: https://stackoverflow.com/questions/55221366
+        val trustLevelEntitySchema = realm.schema.get("TrustLevelEntity")
+
+        /*
+        Creating a new temp field called isLinked which is set to true for those which are
+        references by other objects. Rest of them are set to false. Then removing all
+        those which are false and hence duplicate and unnecessary. Then removing the temp field
+        isLinked
+         */
+        var mainCounter = 0
+        var deviceInfoCounter = 0
+        var keyInfoCounter = 0
+        val deleteCounter: Int
+
+        trustLevelEntitySchema
+                ?.addField("isLinked", Boolean::class.java)
+                ?.transform { obj ->
+                    // Setting to false for all by default
+                    obj.set("isLinked", false)
+                    mainCounter++
+                }
+
+        realm.schema.get("DeviceInfoEntity")?.transform { obj ->
+            // Setting to true for those which are referenced in DeviceInfoEntity
+            deviceInfoCounter++
+            obj.getObject("trustLevelEntity")?.set("isLinked", true)
+        }
+
+        realm.schema.get("KeyInfoEntity")?.transform { obj ->
+            // Setting to true for those which are referenced in KeyInfoEntity
+            keyInfoCounter++
+            obj.getObject("trustLevelEntity")?.set("isLinked", true)
+        }
+
+        // Removing all those which are set as false
+        realm.where("TrustLevelEntity")
+                .equalTo("isLinked", false)
+                .findAll()
+                .also { deleteCounter = it.size }
+                .deleteAllFromRealm()
+
+        trustLevelEntitySchema?.removeField("isLinked")
+
+        Timber.w("TrustLevelEntity cleanup: $mainCounter entities")
+        Timber.w("TrustLevelEntity cleanup: $deviceInfoCounter entities referenced in DeviceInfoEntities")
+        Timber.w("TrustLevelEntity cleanup: $keyInfoCounter entities referenced in KeyInfoEntity")
+        Timber.w("TrustLevelEntity cleanup: $deleteCounter entities deleted!")
+        if (mainCounter != deviceInfoCounter + keyInfoCounter + deleteCounter) {
+            Timber.e("TrustLevelEntity cleanup: Something is not correct...")
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo014.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo014.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f0089e3427727152f852f97cf9cb5713a3ba6050
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo014.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.crypto.store.db.migration
+
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntityFields
+import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+// Version 14L Update the way we remember key sharing
+class MigrateCryptoTo014(realm: DynamicRealm) : RealmMigrator(realm, 14) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.get("SharedSessionEntity")
+                ?.addField(SharedSessionEntityFields.DEVICE_IDENTITY_KEY, String::class.java)
+                ?.addIndex(SharedSessionEntityFields.DEVICE_IDENTITY_KEY)
+                ?.transform {
+                    val sharedUserId = it.getString(SharedSessionEntityFields.USER_ID)
+                    val sharedDeviceId = it.getString(SharedSessionEntityFields.DEVICE_ID)
+                    val knownDevice = realm.where("DeviceInfoEntity")
+                            .equalTo(DeviceInfoEntityFields.USER_ID, sharedUserId)
+                            .equalTo(DeviceInfoEntityFields.DEVICE_ID, sharedDeviceId)
+                            .findFirst()
+                    it.setString(SharedSessionEntityFields.DEVICE_IDENTITY_KEY, knownDevice?.getString(DeviceInfoEntityFields.IDENTITY_KEY))
+                }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo015.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo015.kt
new file mode 100644
index 0000000000000000000000000000000000000000..465c18555a43ff47201591e978de7b1f7bdc8d74
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo015.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.crypto.store.db.migration
+
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
+import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+// Version 15L adds wasEncryptedOnce field to CryptoRoomEntity
+class MigrateCryptoTo015(realm: DynamicRealm) : RealmMigrator(realm, 15) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.get("CryptoRoomEntity")
+                ?.addField(CryptoRoomEntityFields.WAS_ENCRYPTED_ONCE, Boolean::class.java)
+                ?.setNullable(CryptoRoomEntityFields.WAS_ENCRYPTED_ONCE, true)
+                ?.transform {
+                    val currentAlgorithm = it.getString(CryptoRoomEntityFields.ALGORITHM)
+                    it.set(CryptoRoomEntityFields.WAS_ENCRYPTED_ONCE, currentAlgorithm == MXCRYPTO_ALGORITHM_MEGOLM)
+                }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoRoomEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoRoomEntity.kt
index 711b6984641aeecf993503a5d163b62bab9106a6..6167314b5a354db61c9ca734b67bf5cce7b64a43 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoRoomEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoRoomEntity.kt
@@ -27,7 +27,10 @@ internal open class CryptoRoomEntity(
         // Store the current outbound session for this room,
         // to avoid re-create and re-share at each startup (if rotation not needed..)
         // This is specific to megolm but not sure how to model it better
-        var outboundSessionInfo: OutboundGroupSessionInfoEntity? = null
+        var outboundSessionInfo: OutboundGroupSessionInfoEntity? = null,
+        // a security to ensure that a room will never revert to not encrypted
+        // even if a new state event with empty encryption, or state is reset somehow
+        var wasEncryptedOnce: Boolean? = false
         ) :
     RealmObject() {
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt
index d5a96f5ba19d0b7b8aca7b22fed85c1f605ed002..ebc9bcce5ad7b173905eeecd6e0053cd1fcded46 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt
@@ -19,11 +19,9 @@ import com.zhuinden.monarchy.Monarchy
 import io.realm.Realm
 import io.realm.RealmConfiguration
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.asCoroutineDispatcher
 import kotlinx.coroutines.isActive
 import kotlinx.coroutines.launch
-import kotlinx.coroutines.sync.Semaphore
-import kotlinx.coroutines.sync.withPermit
 import kotlinx.coroutines.withContext
 import timber.log.Timber
 
@@ -37,30 +35,26 @@ internal fun <T> CoroutineScope.asyncTransaction(realmConfiguration: RealmConfig
     }
 }
 
-private val realmSemaphore = Semaphore(1)
-
 suspend fun <T> awaitTransaction(config: RealmConfiguration, transaction: suspend (realm: Realm) -> T): T {
-    return realmSemaphore.withPermit {
-        withContext(Dispatchers.IO) {
-            Realm.getInstance(config).use { bgRealm ->
-                bgRealm.beginTransaction()
-                val result: T
-                try {
-                    val start = System.currentTimeMillis()
-                    result = transaction(bgRealm)
-                    if (isActive) {
-                        bgRealm.commitTransaction()
-                        val end = System.currentTimeMillis()
-                        val time = end - start
-                        Timber.v("Execute transaction in $time millis")
-                    }
-                } finally {
-                    if (bgRealm.isInTransaction) {
-                        bgRealm.cancelTransaction()
-                    }
+    return withContext(Realm.WRITE_EXECUTOR.asCoroutineDispatcher()) {
+        Realm.getInstance(config).use { bgRealm ->
+            bgRealm.beginTransaction()
+            val result: T
+            try {
+                val start = System.currentTimeMillis()
+                result = transaction(bgRealm)
+                if (isActive) {
+                    bgRealm.commitTransaction()
+                    val end = System.currentTimeMillis()
+                    val time = end - start
+                    Timber.v("Execute transaction in $time millis")
+                }
+            } finally {
+                if (bgRealm.isInTransaction) {
+                    bgRealm.cancelTransaction()
                 }
-                result
             }
+            result
         }
     }
 }
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 01576c3d611644b80db49b83aadfb9f0ace88a6e..12e60da114541268264c3c84b74c8eafa26b24ce 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
@@ -17,36 +17,32 @@
 package org.matrix.android.sdk.internal.database
 
 import io.realm.DynamicRealm
-import io.realm.FieldAttribute
 import io.realm.RealmMigration
-import org.matrix.android.sdk.api.session.events.model.EventType
-import org.matrix.android.sdk.api.session.room.model.Membership
-import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
-import org.matrix.android.sdk.api.session.room.model.VersioningState
-import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
-import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
-import org.matrix.android.sdk.internal.crypto.model.event.EncryptionEventContent
-import org.matrix.android.sdk.internal.database.model.ChunkEntityFields
-import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields
-import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntityFields
-import org.matrix.android.sdk.internal.database.model.EditionOfEventFields
-import org.matrix.android.sdk.internal.database.model.EventEntityFields
-import org.matrix.android.sdk.internal.database.model.EventInsertEntityFields
-import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
-import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields
-import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntityFields
-import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntityFields
-import org.matrix.android.sdk.internal.database.model.RoomEntityFields
-import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
-import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType
-import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
-import org.matrix.android.sdk.internal.database.model.RoomTagEntityFields
-import org.matrix.android.sdk.internal.database.model.SpaceChildSummaryEntityFields
-import org.matrix.android.sdk.internal.database.model.SpaceParentSummaryEntityFields
-import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
-import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntityFields
-import org.matrix.android.sdk.internal.di.MoshiProvider
-import org.matrix.android.sdk.internal.query.process
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo001
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo002
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo003
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo004
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo005
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo006
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo007
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo008
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo009
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo010
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo011
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo012
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo013
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo014
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo015
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo016
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo017
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo018
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo019
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo020
+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
@@ -54,11 +50,6 @@ import javax.inject.Inject
 internal class RealmSessionStoreMigration @Inject constructor(
         private val normalizer: Normalizer
 ) : RealmMigration {
-
-    companion object {
-        const val SESSION_STORE_SCHEMA_VERSION = 22L
-    }
-
     /**
      * Forces all RealmSessionStoreMigration instances to be equal
      * Avoids Realm throwing when multiple instances of the migration are set
@@ -66,400 +57,35 @@ internal class RealmSessionStoreMigration @Inject constructor(
     override fun equals(other: Any?) = other is RealmSessionStoreMigration
     override fun hashCode() = 1000
 
-    override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
-        Timber.v("Migrating Realm Session from $oldVersion to $newVersion")
-
-        if (oldVersion <= 0) migrateTo1(realm)
-        if (oldVersion <= 1) migrateTo2(realm)
-        if (oldVersion <= 2) migrateTo3(realm)
-        if (oldVersion <= 3) migrateTo4(realm)
-        if (oldVersion <= 4) migrateTo5(realm)
-        if (oldVersion <= 5) migrateTo6(realm)
-        if (oldVersion <= 6) migrateTo7(realm)
-        if (oldVersion <= 7) migrateTo8(realm)
-        if (oldVersion <= 8) migrateTo9(realm)
-        if (oldVersion <= 9) migrateTo10(realm)
-        if (oldVersion <= 10) migrateTo11(realm)
-        if (oldVersion <= 11) migrateTo12(realm)
-        if (oldVersion <= 12) migrateTo13(realm)
-        if (oldVersion <= 13) migrateTo14(realm)
-        if (oldVersion <= 14) migrateTo15(realm)
-        if (oldVersion <= 15) migrateTo16(realm)
-        if (oldVersion <= 16) migrateTo17(realm)
-        if (oldVersion <= 17) migrateTo18(realm)
-        if (oldVersion <= 18) migrateTo19(realm)
-        if (oldVersion <= 19) migrateTo20(realm)
-        if (oldVersion <= 20) migrateTo21(realm)
-        if (oldVersion <= 21) migrateTo22(realm)
-    }
-
-    private fun migrateTo1(realm: DynamicRealm) {
-        Timber.d("Step 0 -> 1")
-        // Add hasFailedSending in RoomSummary and a small warning icon on room list
-
-        realm.schema.get("RoomSummaryEntity")
-                ?.addField(RoomSummaryEntityFields.HAS_FAILED_SENDING, Boolean::class.java)
-                ?.transform { obj ->
-                    obj.setBoolean(RoomSummaryEntityFields.HAS_FAILED_SENDING, false)
-                }
-    }
-
-    private fun migrateTo2(realm: DynamicRealm) {
-        Timber.d("Step 1 -> 2")
-        realm.schema.get("HomeServerCapabilitiesEntity")
-                ?.addField("adminE2EByDefault", Boolean::class.java)
-                ?.transform { obj ->
-                    obj.setBoolean("adminE2EByDefault", true)
-                }
-    }
-
-    private fun migrateTo3(realm: DynamicRealm) {
-        Timber.d("Step 2 -> 3")
-        realm.schema.get("HomeServerCapabilitiesEntity")
-                ?.addField("preferredJitsiDomain", String::class.java)
-                ?.transform { obj ->
-                    // Schedule a refresh of the capabilities
-                    obj.setLong(HomeServerCapabilitiesEntityFields.LAST_UPDATED_TIMESTAMP, 0)
-                }
-    }
-
-    private fun migrateTo4(realm: DynamicRealm) {
-        Timber.d("Step 3 -> 4")
-        realm.schema.create("PendingThreePidEntity")
-                .addField(PendingThreePidEntityFields.CLIENT_SECRET, String::class.java)
-                .setRequired(PendingThreePidEntityFields.CLIENT_SECRET, true)
-                .addField(PendingThreePidEntityFields.EMAIL, String::class.java)
-                .addField(PendingThreePidEntityFields.MSISDN, String::class.java)
-                .addField(PendingThreePidEntityFields.SEND_ATTEMPT, Int::class.java)
-                .addField(PendingThreePidEntityFields.SID, String::class.java)
-                .setRequired(PendingThreePidEntityFields.SID, true)
-                .addField(PendingThreePidEntityFields.SUBMIT_URL, String::class.java)
-    }
-
-    private fun migrateTo5(realm: DynamicRealm) {
-        Timber.d("Step 4 -> 5")
-        realm.schema.get("HomeServerCapabilitiesEntity")
-                ?.removeField("adminE2EByDefault")
-                ?.removeField("preferredJitsiDomain")
-    }
-
-    private fun migrateTo6(realm: DynamicRealm) {
-        Timber.d("Step 5 -> 6")
-        realm.schema.create("PreviewUrlCacheEntity")
-                .addField(PreviewUrlCacheEntityFields.URL, String::class.java)
-                .setRequired(PreviewUrlCacheEntityFields.URL, true)
-                .addPrimaryKey(PreviewUrlCacheEntityFields.URL)
-                .addField(PreviewUrlCacheEntityFields.URL_FROM_SERVER, String::class.java)
-                .addField(PreviewUrlCacheEntityFields.SITE_NAME, String::class.java)
-                .addField(PreviewUrlCacheEntityFields.TITLE, String::class.java)
-                .addField(PreviewUrlCacheEntityFields.DESCRIPTION, String::class.java)
-                .addField(PreviewUrlCacheEntityFields.MXC_URL, String::class.java)
-                .addField(PreviewUrlCacheEntityFields.LAST_UPDATED_TIMESTAMP, Long::class.java)
-    }
-
-    private fun migrateTo7(realm: DynamicRealm) {
-        Timber.d("Step 6 -> 7")
-        realm.schema.get("RoomEntity")
-                ?.addField(RoomEntityFields.MEMBERS_LOAD_STATUS_STR, String::class.java)
-                ?.transform { obj ->
-                    if (obj.getBoolean("areAllMembersLoaded")) {
-                        obj.setString("membersLoadStatusStr", RoomMembersLoadStatusType.LOADED.name)
-                    } else {
-                        obj.setString("membersLoadStatusStr", RoomMembersLoadStatusType.NONE.name)
-                    }
-                }
-                ?.removeField("areAllMembersLoaded")
-    }
-
-    private fun migrateTo8(realm: DynamicRealm) {
-        Timber.d("Step 7 -> 8")
-
-        val editionOfEventSchema = realm.schema.create("EditionOfEvent")
-                .addField(EditionOfEventFields.CONTENT, String::class.java)
-                .addField(EditionOfEventFields.EVENT_ID, String::class.java)
-                .setRequired(EditionOfEventFields.EVENT_ID, true)
-                .addField(EditionOfEventFields.SENDER_ID, String::class.java)
-                .setRequired(EditionOfEventFields.SENDER_ID, true)
-                .addField(EditionOfEventFields.TIMESTAMP, Long::class.java)
-                .addField(EditionOfEventFields.IS_LOCAL_ECHO, Boolean::class.java)
-
-        realm.schema.get("EditAggregatedSummaryEntity")
-                ?.removeField("aggregatedContent")
-                ?.removeField("sourceEvents")
-                ?.removeField("lastEditTs")
-                ?.removeField("sourceLocalEchoEvents")
-                ?.addRealmListField(EditAggregatedSummaryEntityFields.EDITIONS.`$`, editionOfEventSchema)
-
-        // This has to be done once a parent use the model as a child
-        // See https://github.com/realm/realm-java/issues/7402
-        editionOfEventSchema.isEmbedded = true
-    }
-
-    private fun migrateTo9(realm: DynamicRealm) {
-        Timber.d("Step 8 -> 9")
-
-        realm.schema.get("RoomSummaryEntity")
-                ?.addField(RoomSummaryEntityFields.LAST_ACTIVITY_TIME, Long::class.java, FieldAttribute.INDEXED)
-                ?.setNullable(RoomSummaryEntityFields.LAST_ACTIVITY_TIME, true)
-                ?.addIndex(RoomSummaryEntityFields.MEMBERSHIP_STR)
-                ?.addIndex(RoomSummaryEntityFields.IS_DIRECT)
-                ?.addIndex(RoomSummaryEntityFields.VERSIONING_STATE_STR)
-
-                ?.addField(RoomSummaryEntityFields.IS_FAVOURITE, Boolean::class.java)
-                ?.addIndex(RoomSummaryEntityFields.IS_FAVOURITE)
-                ?.addField(RoomSummaryEntityFields.IS_LOW_PRIORITY, Boolean::class.java)
-                ?.addIndex(RoomSummaryEntityFields.IS_LOW_PRIORITY)
-                ?.addField(RoomSummaryEntityFields.IS_SERVER_NOTICE, Boolean::class.java)
-                ?.addIndex(RoomSummaryEntityFields.IS_SERVER_NOTICE)
-
-                ?.transform { obj ->
-                    val isFavorite = obj.getList(RoomSummaryEntityFields.TAGS.`$`).any {
-                        it.getString(RoomTagEntityFields.TAG_NAME) == RoomTag.ROOM_TAG_FAVOURITE
-                    }
-                    obj.setBoolean(RoomSummaryEntityFields.IS_FAVOURITE, isFavorite)
-
-                    val isLowPriority = obj.getList(RoomSummaryEntityFields.TAGS.`$`).any {
-                        it.getString(RoomTagEntityFields.TAG_NAME) == RoomTag.ROOM_TAG_LOW_PRIORITY
-                    }
-
-                    obj.setBoolean(RoomSummaryEntityFields.IS_LOW_PRIORITY, isLowPriority)
-
-//                    XXX migrate last message origin server ts
-                    obj.getObject(RoomSummaryEntityFields.LATEST_PREVIEWABLE_EVENT.`$`)
-                            ?.getObject(TimelineEventEntityFields.ROOT.`$`)
-                            ?.getLong(EventEntityFields.ORIGIN_SERVER_TS)?.let {
-                                obj.setLong(RoomSummaryEntityFields.LAST_ACTIVITY_TIME, it)
-                            }
-                }
-    }
-
-    private fun migrateTo10(realm: DynamicRealm) {
-        Timber.d("Step 9 -> 10")
-        realm.schema.create("SpaceChildSummaryEntity")
-                ?.addField(SpaceChildSummaryEntityFields.ORDER, String::class.java)
-                ?.addField(SpaceChildSummaryEntityFields.CHILD_ROOM_ID, String::class.java)
-                ?.addField(SpaceChildSummaryEntityFields.AUTO_JOIN, Boolean::class.java)
-                ?.setNullable(SpaceChildSummaryEntityFields.AUTO_JOIN, true)
-                ?.addRealmObjectField(SpaceChildSummaryEntityFields.CHILD_SUMMARY_ENTITY.`$`, realm.schema.get("RoomSummaryEntity")!!)
-                ?.addRealmListField(SpaceChildSummaryEntityFields.VIA_SERVERS.`$`, String::class.java)
-
-        realm.schema.create("SpaceParentSummaryEntity")
-                ?.addField(SpaceParentSummaryEntityFields.PARENT_ROOM_ID, String::class.java)
-                ?.addField(SpaceParentSummaryEntityFields.CANONICAL, Boolean::class.java)
-                ?.setNullable(SpaceParentSummaryEntityFields.CANONICAL, true)
-                ?.addRealmObjectField(SpaceParentSummaryEntityFields.PARENT_SUMMARY_ENTITY.`$`, realm.schema.get("RoomSummaryEntity")!!)
-                ?.addRealmListField(SpaceParentSummaryEntityFields.VIA_SERVERS.`$`, String::class.java)
-
-        val creationContentAdapter = MoshiProvider.providesMoshi().adapter(RoomCreateContent::class.java)
-        realm.schema.get("RoomSummaryEntity")
-                ?.addField(RoomSummaryEntityFields.ROOM_TYPE, String::class.java)
-                ?.addField(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, String::class.java)
-                ?.addField(RoomSummaryEntityFields.GROUP_IDS, String::class.java)
-                ?.transform { obj ->
-
-                    val creationEvent = realm.where("CurrentStateEventEntity")
-                            .equalTo(CurrentStateEventEntityFields.ROOM_ID, obj.getString(RoomSummaryEntityFields.ROOM_ID))
-                            .equalTo(CurrentStateEventEntityFields.TYPE, EventType.STATE_ROOM_CREATE)
-                            .findFirst()
-
-                    val roomType = creationEvent?.getObject(CurrentStateEventEntityFields.ROOT.`$`)
-                            ?.getString(EventEntityFields.CONTENT)?.let {
-                                creationContentAdapter.fromJson(it)?.type
-                            }
-
-                    obj.setString(RoomSummaryEntityFields.ROOM_TYPE, roomType)
-                }
-                ?.addRealmListField(RoomSummaryEntityFields.PARENTS.`$`, realm.schema.get("SpaceParentSummaryEntity")!!)
-                ?.addRealmListField(RoomSummaryEntityFields.CHILDREN.`$`, realm.schema.get("SpaceChildSummaryEntity")!!)
-    }
+    val schemaVersion = 25L
 
-    private fun migrateTo11(realm: DynamicRealm) {
-        Timber.d("Step 10 -> 11")
-        realm.schema.get("EventEntity")
-                ?.addField(EventEntityFields.SEND_STATE_DETAILS, String::class.java)
-    }
-
-    private fun migrateTo12(realm: DynamicRealm) {
-        Timber.d("Step 11 -> 12")
-
-        val joinRulesContentAdapter = MoshiProvider.providesMoshi().adapter(RoomJoinRulesContent::class.java)
-        realm.schema.get("RoomSummaryEntity")
-                ?.addField(RoomSummaryEntityFields.JOIN_RULES_STR, String::class.java)
-                ?.transform { obj ->
-                    val joinRulesEvent = realm.where("CurrentStateEventEntity")
-                            .equalTo(CurrentStateEventEntityFields.ROOM_ID, obj.getString(RoomSummaryEntityFields.ROOM_ID))
-                            .equalTo(CurrentStateEventEntityFields.TYPE, EventType.STATE_ROOM_JOIN_RULES)
-                            .findFirst()
-
-                    val roomJoinRules = joinRulesEvent?.getObject(CurrentStateEventEntityFields.ROOT.`$`)
-                            ?.getString(EventEntityFields.CONTENT)?.let {
-                                joinRulesContentAdapter.fromJson(it)?.joinRules
-                            }
-
-                    obj.setString(RoomSummaryEntityFields.JOIN_RULES_STR, roomJoinRules?.name)
-                }
-
-        realm.schema.get("SpaceChildSummaryEntity")
-                ?.addField(SpaceChildSummaryEntityFields.SUGGESTED, Boolean::class.java)
-                ?.setNullable(SpaceChildSummaryEntityFields.SUGGESTED, true)
-    }
-
-    private fun migrateTo13(realm: DynamicRealm) {
-        Timber.d("Step 12 -> 13")
-        // Fix issue with the nightly build. Eventually play again the migration which has been included in migrateTo12()
-        realm.schema.get("SpaceChildSummaryEntity")
-                ?.takeIf { !it.hasField(SpaceChildSummaryEntityFields.SUGGESTED) }
-                ?.addField(SpaceChildSummaryEntityFields.SUGGESTED, Boolean::class.java)
-                ?.setNullable(SpaceChildSummaryEntityFields.SUGGESTED, true)
-    }
-
-    private fun migrateTo14(realm: DynamicRealm) {
-        Timber.d("Step 13 -> 14")
-        val roomAccountDataSchema = realm.schema.create("RoomAccountDataEntity")
-                .addField(RoomAccountDataEntityFields.CONTENT_STR, String::class.java)
-                .addField(RoomAccountDataEntityFields.TYPE, String::class.java, FieldAttribute.INDEXED)
-
-        realm.schema.get("RoomEntity")
-                ?.addRealmListField(RoomEntityFields.ACCOUNT_DATA.`$`, roomAccountDataSchema)
-
-        realm.schema.get("RoomSummaryEntity")
-                ?.addField(RoomSummaryEntityFields.IS_HIDDEN_FROM_USER, Boolean::class.java, FieldAttribute.INDEXED)
-                ?.transform {
-                    val isHiddenFromUser = it.getString(RoomSummaryEntityFields.VERSIONING_STATE_STR) == VersioningState.UPGRADED_ROOM_JOINED.name
-                    it.setBoolean(RoomSummaryEntityFields.IS_HIDDEN_FROM_USER, isHiddenFromUser)
-                }
-
-        roomAccountDataSchema.isEmbedded = true
-    }
-
-    private fun migrateTo15(realm: DynamicRealm) {
-        Timber.d("Step 14 -> 15")
-        // fix issue with flattenParentIds on DM that kept growing with duplicate
-        // so we reset it, will be updated next sync
-        realm.where("RoomSummaryEntity")
-                .process(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.activeMemberships())
-                .equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
-                .findAll()
-                .onEach {
-                    it.setString(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, null)
-                }
-    }
-
-    private fun migrateTo16(realm: DynamicRealm) {
-        Timber.d("Step 15 -> 16")
-        realm.schema.get("HomeServerCapabilitiesEntity")
-                ?.addField(HomeServerCapabilitiesEntityFields.ROOM_VERSIONS_JSON, String::class.java)
-                ?.transform { obj ->
-                    // Schedule a refresh of the capabilities
-                    obj.setLong(HomeServerCapabilitiesEntityFields.LAST_UPDATED_TIMESTAMP, 0)
-                }
-    }
-
-    private fun migrateTo17(realm: DynamicRealm) {
-        Timber.d("Step 16 -> 17")
-        realm.schema.get("EventInsertEntity")
-                ?.addField(EventInsertEntityFields.CAN_BE_PROCESSED, Boolean::class.java)
-    }
-
-    private fun migrateTo18(realm: DynamicRealm) {
-        Timber.d("Step 17 -> 18")
-        realm.schema.create("UserPresenceEntity")
-                ?.addField(UserPresenceEntityFields.USER_ID, String::class.java)
-                ?.addPrimaryKey(UserPresenceEntityFields.USER_ID)
-                ?.setRequired(UserPresenceEntityFields.USER_ID, true)
-                ?.addField(UserPresenceEntityFields.PRESENCE_STR, String::class.java)
-                ?.addField(UserPresenceEntityFields.LAST_ACTIVE_AGO, Long::class.java)
-                ?.setNullable(UserPresenceEntityFields.LAST_ACTIVE_AGO, true)
-                ?.addField(UserPresenceEntityFields.STATUS_MESSAGE, String::class.java)
-                ?.addField(UserPresenceEntityFields.IS_CURRENTLY_ACTIVE, Boolean::class.java)
-                ?.setNullable(UserPresenceEntityFields.IS_CURRENTLY_ACTIVE, true)
-                ?.addField(UserPresenceEntityFields.AVATAR_URL, String::class.java)
-                ?.addField(UserPresenceEntityFields.DISPLAY_NAME, String::class.java)
-
-        val userPresenceEntity = realm.schema.get("UserPresenceEntity") ?: return
-        realm.schema.get("RoomSummaryEntity")
-                ?.addRealmObjectField(RoomSummaryEntityFields.DIRECT_USER_PRESENCE.`$`, userPresenceEntity)
-
-        realm.schema.get("RoomMemberSummaryEntity")
-                ?.addRealmObjectField(RoomMemberSummaryEntityFields.USER_PRESENCE_ENTITY.`$`, userPresenceEntity)
-    }
-
-    private fun migrateTo19(realm: DynamicRealm) {
-        Timber.d("Step 18 -> 19")
-        realm.schema.get("RoomSummaryEntity")
-                ?.addField(RoomSummaryEntityFields.NORMALIZED_DISPLAY_NAME, String::class.java)
-                ?.transform {
-                    it.getString(RoomSummaryEntityFields.DISPLAY_NAME)?.let { displayName ->
-                        val normalised = normalizer.normalize(displayName)
-                        it.set(RoomSummaryEntityFields.NORMALIZED_DISPLAY_NAME, normalised)
-                    }
-                }
-    }
-
-    private fun migrateTo20(realm: DynamicRealm) {
-        Timber.d("Step 19 -> 20")
-
-        realm.schema.get("ChunkEntity")?.apply {
-            if (hasField("numberOfTimelineEvents")) {
-                removeField("numberOfTimelineEvents")
-            }
-            var cleanOldChunks = false
-            if (!hasField(ChunkEntityFields.NEXT_CHUNK.`$`)) {
-                cleanOldChunks = true
-                addRealmObjectField(ChunkEntityFields.NEXT_CHUNK.`$`, this)
-            }
-            if (!hasField(ChunkEntityFields.PREV_CHUNK.`$`)) {
-                cleanOldChunks = true
-                addRealmObjectField(ChunkEntityFields.PREV_CHUNK.`$`, this)
-            }
-            if (cleanOldChunks) {
-                val chunkEntities = realm.where("ChunkEntity").equalTo(ChunkEntityFields.IS_LAST_FORWARD, false).findAll()
-                chunkEntities.deleteAllFromRealm()
-            }
-        }
-    }
-
-    private fun migrateTo21(realm: DynamicRealm) {
-        Timber.d("Step 20 -> 21")
-
-        realm.schema.get("RoomSummaryEntity")
-                ?.addField(RoomSummaryEntityFields.E2E_ALGORITHM, String::class.java)
-                ?.transform { obj ->
-
-                    val encryptionContentAdapter = MoshiProvider.providesMoshi().adapter(EncryptionEventContent::class.java)
-
-                    val encryptionEvent = realm.where("CurrentStateEventEntity")
-                            .equalTo(CurrentStateEventEntityFields.ROOM_ID, obj.getString(RoomSummaryEntityFields.ROOM_ID))
-                            .equalTo(CurrentStateEventEntityFields.TYPE, EventType.STATE_ROOM_ENCRYPTION)
-                            .findFirst()
-
-                    val encryptionEventRoot = encryptionEvent?.getObject(CurrentStateEventEntityFields.ROOT.`$`)
-                    val algorithm = encryptionEventRoot
-                            ?.getString(EventEntityFields.CONTENT)?.let {
-                                encryptionContentAdapter.fromJson(it)?.algorithm
-                            }
-
-                    obj.setString(RoomSummaryEntityFields.E2E_ALGORITHM, algorithm)
-                    obj.setBoolean(RoomSummaryEntityFields.IS_ENCRYPTED, encryptionEvent != null)
-                    encryptionEventRoot?.getLong(EventEntityFields.ORIGIN_SERVER_TS)?.let {
-                        obj.setLong(RoomSummaryEntityFields.ENCRYPTION_EVENT_TS, it)
-                    }
-                }
-    }
-
-    private fun migrateTo22(realm: DynamicRealm) {
-        Timber.d("Step 21 -> 22")
-        val listJoinedRoomIds = realm.where("RoomEntity")
-                .equalTo(RoomEntityFields.MEMBERSHIP_STR, Membership.JOIN.name).findAll()
-                .map { it.getString(RoomEntityFields.ROOM_ID) }
-
-        val hasMissingStateEvent = realm.where("CurrentStateEventEntity")
-                .`in`(CurrentStateEventEntityFields.ROOM_ID, listJoinedRoomIds.toTypedArray())
-                .isNull(CurrentStateEventEntityFields.ROOT.`$`).findFirst() != null
-
-        if (hasMissingStateEvent) {
-            Timber.v("Has some missing state event, clear session cache")
-            realm.deleteAll()
-        }
+    override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
+        Timber.d("Migrating Realm Session from $oldVersion to $newVersion")
+
+        if (oldVersion < 1) MigrateSessionTo001(realm).perform()
+        if (oldVersion < 2) MigrateSessionTo002(realm).perform()
+        if (oldVersion < 3) MigrateSessionTo003(realm).perform()
+        if (oldVersion < 4) MigrateSessionTo004(realm).perform()
+        if (oldVersion < 5) MigrateSessionTo005(realm).perform()
+        if (oldVersion < 6) MigrateSessionTo006(realm).perform()
+        if (oldVersion < 7) MigrateSessionTo007(realm).perform()
+        if (oldVersion < 8) MigrateSessionTo008(realm).perform()
+        if (oldVersion < 9) MigrateSessionTo009(realm).perform()
+        if (oldVersion < 10) MigrateSessionTo010(realm).perform()
+        if (oldVersion < 11) MigrateSessionTo011(realm).perform()
+        if (oldVersion < 12) MigrateSessionTo012(realm).perform()
+        if (oldVersion < 13) MigrateSessionTo013(realm).perform()
+        if (oldVersion < 14) MigrateSessionTo014(realm).perform()
+        if (oldVersion < 15) MigrateSessionTo015(realm).perform()
+        if (oldVersion < 16) MigrateSessionTo016(realm).perform()
+        if (oldVersion < 17) MigrateSessionTo017(realm).perform()
+        if (oldVersion < 18) MigrateSessionTo018(realm).perform()
+        if (oldVersion < 19) MigrateSessionTo019(realm, normalizer).perform()
+        if (oldVersion < 20) MigrateSessionTo020(realm).perform()
+        if (oldVersion < 21) MigrateSessionTo021(realm).perform()
+        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/SessionRealmConfigurationFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/SessionRealmConfigurationFactory.kt
index 04ca26a943da6092ad096e218fbd312f4534f8b9..08d55b5647fac964c05945963e98599f3c67a705 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/SessionRealmConfigurationFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/SessionRealmConfigurationFactory.kt
@@ -71,7 +71,7 @@ internal class SessionRealmConfigurationFactory @Inject constructor(
                 }
                 .allowWritesOnUiThread(true)
                 .modules(SessionRealmModule())
-                .schemaVersion(RealmSessionStoreMigration.SESSION_STORE_SCHEMA_VERSION)
+                .schemaVersion(realmSessionStoreMigration.schemaVersion)
                 .migration(realmSessionStoreMigration)
                 .build()
 
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 c21bf74d93440a2a96350676d467cc7f86ddfc03..289db9fa15b4351adc99ae2df221d3fd619cbd60 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
@@ -34,6 +34,7 @@ import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
 import org.matrix.android.sdk.internal.database.query.find
 import org.matrix.android.sdk.internal.database.query.getOrCreate
 import org.matrix.android.sdk.internal.database.query.where
+import org.matrix.android.sdk.internal.database.query.whereRoomId
 import org.matrix.android.sdk.internal.extensions.assertIsManaged
 import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
 import timber.log.Timber
@@ -81,7 +82,7 @@ internal fun ChunkEntity.addStateEvent(roomId: String, stateEvent: EventEntity,
 internal fun ChunkEntity.addTimelineEvent(roomId: String,
                                           eventEntity: EventEntity,
                                           direction: PaginationDirection,
-                                          roomMemberContentsByUser: Map<String, RoomMemberContent?>) {
+                                          roomMemberContentsByUser: Map<String, RoomMemberContent?>? = null) {
     val eventId = eventEntity.eventId
     if (timelineEvents.find(eventId) != null) {
         return
@@ -101,7 +102,7 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String,
                 ?.also { it.cleanUp(eventEntity.sender) }
         this.readReceipts = readReceiptsSummaryEntity
         this.displayIndex = displayIndex
-        val roomMemberContent = roomMemberContentsByUser[senderId]
+        val roomMemberContent = roomMemberContentsByUser?.get(senderId)
         this.senderAvatar = roomMemberContent?.avatarUrl
         this.senderName = roomMemberContent?.displayName
         isUniqueDisplayName = if (roomMemberContent?.displayName != null) {
@@ -157,9 +158,21 @@ private fun ChunkEntity.addTimelineEventFromMerge(realm: Realm, timelineEventEnt
         this.senderName = timelineEventEntity.senderName
         this.isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName
     }
+    handleThreadSummary(realm, eventId, copied)
     timelineEvents.add(copied)
 }
 
+/**
+ * Upon copy of the timeline events we should update the latestMessage TimelineEventEntity with the new one
+ */
+private fun handleThreadSummary(realm: Realm, oldEventId: String, newTimelineEventEntity: TimelineEventEntity) {
+    EventEntity
+            .whereRoomId(realm, newTimelineEventEntity.roomId)
+            .equalTo(EventEntityFields.IS_ROOT_THREAD, true)
+            .equalTo(EventEntityFields.THREAD_SUMMARY_LATEST_MESSAGE.EVENT_ID, oldEventId)
+            .findFirst()?.threadSummaryLatestMessage = newTimelineEventEntity
+}
+
 private fun handleReadReceipts(realm: Realm, roomId: String, eventEntity: EventEntity, senderId: String): ReadReceiptsSummaryEntity {
     val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventEntity.eventId).findFirst()
             ?: realm.createObject<ReadReceiptsSummaryEntity>(eventEntity.eventId).apply {
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
new file mode 100644
index 0000000000000000000000000000000000000000..f703bfaf82adcac49a4c96697ae90b099ac7267d
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
@@ -0,0 +1,321 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.database.helper
+
+import io.realm.Realm
+import io.realm.RealmQuery
+import io.realm.Sort
+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.mapper.asDomain
+import org.matrix.android.sdk.internal.database.model.ChunkEntity
+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.ReadReceiptEntity
+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.find
+import org.matrix.android.sdk.internal.database.query.findIncludingEvent
+import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
+import org.matrix.android.sdk.internal.database.query.where
+import org.matrix.android.sdk.internal.database.query.whereRoomId
+
+private typealias ThreadSummary = Pair<Int, TimelineEventEntity>?
+
+/**
+ * Finds the root thread event and update it with the latest message summary along with the number
+ * of threads included. If there is no root thread event no action is done
+ */
+internal fun Map<String, EventEntity>.updateThreadSummaryIfNeeded(
+        roomId: String,
+        realm: Realm, currentUserId: String,
+        chunkEntity: ChunkEntity? = null,
+        shouldUpdateNotifications: Boolean = true) {
+    for ((rootThreadEventId, eventEntity) in this) {
+        eventEntity.threadSummaryInThread(eventEntity.realm, rootThreadEventId, chunkEntity)?.let { threadSummary ->
+
+            val numberOfMessages = threadSummary.first
+            val latestEventInThread = threadSummary.second
+
+            // If this is a thread message, find its root event if exists
+            val rootThreadEvent = if (eventEntity.isThread()) eventEntity.findRootThreadEvent() else eventEntity
+
+            rootThreadEvent?.markEventAsRoot(
+                    threadsCounted = numberOfMessages,
+                    latestMessageTimelineEventEntity = latestEventInThread
+            )
+        }
+    }
+
+    if (shouldUpdateNotifications) {
+        updateNotificationsNew(roomId, realm, currentUserId)
+    }
+}
+
+/**
+ * Finds the root event of the the current thread event message.
+ * Returns the EventEntity or null if the root event do not exist
+ */
+internal fun EventEntity.findRootThreadEvent(): EventEntity? =
+        rootThreadEventId?.let {
+            EventEntity
+                    .where(realm, it)
+                    .findFirst()
+        }
+
+/**
+ * Mark or update the current event a root thread event
+ */
+internal fun EventEntity.markEventAsRoot(
+        threadsCounted: Int,
+        latestMessageTimelineEventEntity: TimelineEventEntity?) {
+    isRootThread = true
+    numberOfThreads = threadsCounted
+    threadSummaryLatestMessage = latestMessageTimelineEventEntity
+}
+
+/**
+ * Count the number of threads for the provided root thread eventId, and finds the latest event message
+ * @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 {
+    // Number of messages
+    val messages = TimelineEventEntity
+            .whereRoomId(realm, roomId = roomId)
+            .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId)
+            .count()
+            .toInt()
+
+    if (messages <= 0) return null
+
+    // Find latest thread event, we know it exists
+    var chunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId) ?: chunkEntity ?: return null
+    var result: TimelineEventEntity? = null
+
+    // Iterate the chunk until we find our latest event
+    while (result == null) {
+        result = findLatestSortedChunkEvent(chunk, rootThreadEventId)
+        chunk = ChunkEntity.find(realm, roomId, nextToken = chunk.prevToken) ?: break
+    }
+
+    if (result == null && chunkEntity != null) {
+        // Find latest event from our current chunk
+        result = findLatestSortedChunkEvent(chunkEntity, rootThreadEventId)
+    } else if (result != null && chunkEntity != null) {
+        val currentChunkLatestEvent = findLatestSortedChunkEvent(chunkEntity, rootThreadEventId)
+        result = findMostRecentEvent(result, currentChunkLatestEvent)
+    }
+
+    result ?: return null
+
+    return ThreadSummary(messages, result)
+}
+
+/**
+ * Lets compare them in case user is moving forward in the timeline and we cannot know the
+ * exact chunk sequence while currentChunk is not yet committed in the DB
+ */
+private fun findMostRecentEvent(result: TimelineEventEntity, currentChunkLatestEvent: TimelineEventEntity?): TimelineEventEntity {
+    currentChunkLatestEvent ?: return result
+    val currentChunkEventTimestamp = currentChunkLatestEvent.root?.originServerTs ?: return result
+    val resultTimestamp = result.root?.originServerTs ?: return result
+    if (currentChunkEventTimestamp > resultTimestamp) {
+        return currentChunkLatestEvent
+    }
+    return result
+}
+
+/**
+ * Find the latest event of the current chunk
+ */
+private fun findLatestSortedChunkEvent(chunk: ChunkEntity, rootThreadEventId: String): TimelineEventEntity? =
+        chunk.timelineEvents.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)?.firstOrNull {
+            it.root?.rootThreadEventId == rootThreadEventId
+        }
+
+/**
+ * Find all TimelineEventEntity that are root threads for the specified room
+ * @param roomId The room that all stored root threads will be returned
+ */
+internal fun TimelineEventEntity.Companion.findAllThreadsForRoomId(realm: Realm, roomId: String): RealmQuery<TimelineEventEntity> =
+        TimelineEventEntity
+                .whereRoomId(realm, roomId = roomId)
+                .equalTo(TimelineEventEntityFields.ROOT.IS_ROOT_THREAD, true)
+                .sort("${TimelineEventEntityFields.ROOT.THREAD_SUMMARY_LATEST_MESSAGE}.${TimelineEventEntityFields.ROOT.ORIGIN_SERVER_TS}", Sort.DESCENDING)
+
+/**
+ * Map each root thread TimelineEvent with the equivalent decrypted text edition/replacement
+ */
+internal fun List<TimelineEvent>.mapEventsWithEdition(realm: Realm, roomId: String): List<TimelineEvent> =
+        this.map {
+            EventAnnotationsSummaryEntity
+                    .where(realm, roomId, eventId = it.eventId)
+                    .findFirst()
+                    ?.editSummary
+                    ?.editions
+                    ?.lastOrNull()
+                    ?.eventId
+                    ?.let { editedEventId ->
+                        TimelineEventEntity.where(realm, roomId, eventId = editedEventId).findFirst()?.let { editedEvent ->
+                            it.root.threadDetails = it.root.threadDetails?.copy(lastRootThreadEdition = editedEvent.root?.asDomain()?.getDecryptedTextSummary()
+                                    ?: "(edited)")
+                            it
+                        } ?: it
+                    } ?: it
+        }
+
+/**
+ * Returns a list of all the marked unread threads that exists for the specified room
+ * @param roomId The roomId that the user is currently in
+ */
+internal fun TimelineEventEntity.Companion.findAllLocalThreadNotificationsForRoomId(realm: Realm, roomId: String): RealmQuery<TimelineEventEntity> =
+        TimelineEventEntity
+                .whereRoomId(realm, roomId = roomId)
+                .equalTo(TimelineEventEntityFields.ROOT.IS_ROOT_THREAD, true)
+                .beginGroup()
+                .equalTo(TimelineEventEntityFields.ROOT.THREAD_NOTIFICATION_STATE_STR, ThreadNotificationState.NEW_MESSAGE.name)
+                .or()
+                .equalTo(TimelineEventEntityFields.ROOT.THREAD_NOTIFICATION_STATE_STR, ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE.name)
+                .endGroup()
+
+/**
+ * Returns whether or not the given user is participating in a current thread
+ * @param roomId the room that the thread exists
+ * @param rootThreadEventId the thread that the search will be done
+ * @param senderId the user that will try to find participation
+ */
+internal fun TimelineEventEntity.Companion.isUserParticipatingInThread(realm: Realm, roomId: String, rootThreadEventId: String, senderId: String): Boolean =
+        TimelineEventEntity
+                .whereRoomId(realm, roomId = roomId)
+                .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId)
+                .equalTo(TimelineEventEntityFields.ROOT.SENDER, senderId)
+                .findFirst()
+                ?.let { true }
+                ?: false
+
+/**
+ * Returns whether or not the given user is mentioned in a current thread
+ * @param roomId the room that the thread exists
+ * @param rootThreadEventId the thread that the search will be done
+ * @param userId the user that will try to find if there is a mention
+ */
+internal fun TimelineEventEntity.Companion.isUserMentionedInThread(realm: Realm, roomId: String, rootThreadEventId: String, userId: String): Boolean =
+        TimelineEventEntity
+                .whereRoomId(realm, roomId = roomId)
+                .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId)
+                .equalTo(TimelineEventEntityFields.ROOT.SENDER, userId)
+                .findAll()
+                .firstOrNull { isUserMentioned(userId, it) }
+                ?.let { true }
+                ?: false
+
+/**
+ * Find the read receipt for the current user
+ */
+internal fun findMyReadReceipt(realm: Realm, roomId: String, userId: String): String? =
+        ReadReceiptEntity.where(realm, roomId = roomId, userId = userId)
+                .findFirst()
+                ?.eventId
+
+/**
+ * Returns whether or not the user is mentioned in the event
+ */
+internal fun isUserMentioned(currentUserId: String, timelineEventEntity: TimelineEventEntity?): Boolean {
+    return timelineEventEntity?.root?.asDomain()?.isUserMentioned(currentUserId) == true
+}
+
+/**
+ * Update badge notifications. Count the number of new thread events after the latest
+ * read receipt and aggregate. This function will find and notify new thread events
+ * that the user is either mentioned, or the user had participated in.
+ * Important: If the root thread event is not fetched notification will not work
+ * Important: It will work only with the latest chunk, while read marker will be changed
+ * immediately so we should not display wrong notifications
+ */
+internal fun updateNotificationsNew(roomId: String, realm: Realm, currentUserId: String) {
+    val readReceipt = findMyReadReceipt(realm, roomId, currentUserId) ?: return
+
+    val readReceiptChunk = ChunkEntity
+            .findIncludingEvent(realm, readReceipt) ?: return
+
+    val readReceiptChunkTimelineEvents = readReceiptChunk
+            .timelineEvents
+            .where()
+            .equalTo(TimelineEventEntityFields.ROOM_ID, roomId)
+            .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
+            .findAll() ?: return
+
+    val readReceiptChunkPosition = readReceiptChunkTimelineEvents.indexOfFirst { it.eventId == readReceipt }
+
+    if (readReceiptChunkPosition == -1) return
+
+    if (readReceiptChunkPosition < readReceiptChunkTimelineEvents.lastIndex) {
+        // If the read receipt is found inside the chunk
+
+        val threadEventsAfterReadReceipt = readReceiptChunkTimelineEvents
+                .slice(readReceiptChunkPosition..readReceiptChunkTimelineEvents.lastIndex)
+                .filter { it.root?.isThread() == true }
+
+        // In order for the below code to work for old events, we should save the previous read receipt
+        // and then continue with the chunk search for that read receipt
+        /*
+       val newThreadEventsList = arrayListOf<TimelineEventEntity>()
+        newThreadEventsList.addAll(threadEventsAfterReadReceipt)
+
+        // got from latest chunk all new threads, lets move to the others
+        var nextChunk = ChunkEntity
+                .find(realm = realm, roomId = roomId, nextToken = readReceiptChunk.nextToken)
+                .takeIf { readReceiptChunk.nextToken != null }
+        while (nextChunk != null) {
+            newThreadEventsList.addAll(nextChunk.timelineEvents
+                    .filter { it.root?.isThread() == true })
+            nextChunk = ChunkEntity
+                    .find(realm = realm, roomId = roomId, nextToken = nextChunk.nextToken)
+                    .takeIf { readReceiptChunk.nextToken != null }
+        }*/
+
+        // Find if the user is mentioned in those events
+        val userMentionsList = threadEventsAfterReadReceipt
+                .filter {
+                    isUserMentioned(currentUserId = currentUserId, it)
+                }.map {
+                    it.root?.rootThreadEventId
+                }
+
+        // Find the root events in the new thread events
+        val rootThreads = threadEventsAfterReadReceipt.distinctBy { it.root?.rootThreadEventId }.mapNotNull { it.root?.rootThreadEventId }
+
+        // Update root thread events only if the user have participated in
+        rootThreads.forEach { eventId ->
+            val isUserParticipating = TimelineEventEntity.isUserParticipatingInThread(
+                    realm = realm,
+                    roomId = roomId,
+                    rootThreadEventId = eventId,
+                    senderId = currentUserId)
+            val rootThreadEventEntity = EventEntity.where(realm, eventId).findFirst()
+
+            if (isUserParticipating) {
+                rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_MESSAGE
+            }
+
+            if (userMentionsList.contains(eventId)) {
+                rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE
+            }
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/lightweight/LightweightSettingsStorage.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/lightweight/LightweightSettingsStorage.kt
new file mode 100644
index 0000000000000000000000000000000000000000..700b94a98582b1600e25b9b18d7678099c50c7a2
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/lightweight/LightweightSettingsStorage.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.lightweight
+
+import android.content.Context
+import androidx.core.content.edit
+import androidx.preference.PreferenceManager
+import javax.inject.Inject
+
+/**
+ * The purpose of this class is to provide an alternative and lightweight way to store settings/data
+ * on the sdi without using the database. This should be used just for sdk/user preferences and
+ * not for large data sets
+ */
+
+class LightweightSettingsStorage  @Inject constructor(context: Context) {
+
+    private val sdkDefaultPrefs = PreferenceManager.getDefaultSharedPreferences(context.applicationContext)
+
+    fun setThreadMessagesEnabled(enabled: Boolean) {
+        sdkDefaultPrefs.edit {
+            putBoolean(MATRIX_SDK_SETTINGS_THREAD_MESSAGES_ENABLED, enabled)
+        }
+    }
+
+    fun areThreadMessagesEnabled(): Boolean {
+        return sdkDefaultPrefs.getBoolean(MATRIX_SDK_SETTINGS_THREAD_MESSAGES_ENABLED, false)
+    }
+
+    companion object {
+        const val MATRIX_SDK_SETTINGS_THREAD_MESSAGES_ENABLED = "MATRIX_SDK_SETTINGS_THREAD_MESSAGES_ENABLED"
+    }
+}
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 613b38e3403b1023bc9a9bb9fa46b8b23311c6c7..9c420e81fd70759fdaf70876d43e79fe755daddd 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
@@ -21,7 +21,11 @@ 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.UnsignedData
+import org.matrix.android.sdk.api.session.events.model.getRootThreadEventId
 import org.matrix.android.sdk.api.session.room.send.SendState
+import org.matrix.android.sdk.api.session.room.sender.SenderInfo
+import org.matrix.android.sdk.api.session.threads.ThreadDetails
+import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
 import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
 import org.matrix.android.sdk.internal.database.model.EventEntity
 import org.matrix.android.sdk.internal.di.MoshiProvider
@@ -51,6 +55,10 @@ internal object EventMapper {
         }
         eventEntity.decryptionErrorReason = event.mCryptoErrorReason
         eventEntity.decryptionErrorCode = event.mCryptoError?.name
+        eventEntity.isRootThread = event.threadDetails?.isRootThread ?: false
+        eventEntity.rootThreadEventId = event.getRootThreadEventId()
+        eventEntity.numberOfThreads = event.threadDetails?.numberOfThreads ?: 0
+        eventEntity.threadNotificationState = event.threadDetails?.threadNotificationState ?: ThreadNotificationState.NO_NEW_MESSAGE
         return eventEntity
     }
 
@@ -93,6 +101,23 @@ internal object EventMapper {
                 MXCryptoError.ErrorType.valueOf(errorCode)
             }
             it.mCryptoErrorReason = eventEntity.decryptionErrorReason
+            it.threadDetails = ThreadDetails(
+                    isRootThread = eventEntity.isRootThread,
+                    isThread = if (it.threadDetails?.isThread == true) true else eventEntity.isThread(),
+                    numberOfThreads = eventEntity.numberOfThreads,
+                    threadSummarySenderInfo = eventEntity.threadSummaryLatestMessage?.let { timelineEventEntity ->
+                        SenderInfo(
+                                userId = timelineEventEntity.root?.sender ?: "",
+                                displayName = timelineEventEntity.senderName,
+                                isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName,
+                                avatarUrl = timelineEventEntity.senderAvatar
+                        )
+                    },
+                    threadNotificationState = eventEntity.threadNotificationState,
+                    threadSummaryLatestTextMessage = eventEntity.threadSummaryLatestMessage?.root?.asDomain()?.getDecryptedTextSummary(),
+                    lastMessageTimestamp = eventEntity.threadSummaryLatestMessage?.root?.originServerTs
+
+            )
         }
     }
 }
@@ -101,9 +126,15 @@ internal fun EventEntity.asDomain(castJsonNumbers: Boolean = false): Event {
     return EventMapper.map(this, castJsonNumbers)
 }
 
-internal fun Event.toEntity(roomId: String, sendState: SendState, ageLocalTs: Long?): EventEntity {
+internal fun Event.toEntity(roomId: String, sendState: SendState, ageLocalTs: Long?, contentToInject: String? = null): EventEntity {
     return EventMapper.map(this, roomId).apply {
         this.sendState = sendState
         this.ageLocalTs = ageLocalTs
+        contentToInject?.let {
+            this.content = it
+            if (this.type == EventType.STICKER) {
+                this.type = EventType.MESSAGE
+            }
+        }
     }
 }
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 8b6d263f8c7e7bbf6ffe3bd4ed58e689d36451f6..7869506015e282845a4dab84bd17ed19f6a1fd2c 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
@@ -35,6 +35,9 @@ internal object HomeServerCapabilitiesMapper {
     fun map(entity: HomeServerCapabilitiesEntity): HomeServerCapabilities {
         return HomeServerCapabilities(
                 canChangePassword = entity.canChangePassword,
+                canChangeDisplayName = entity.canChangeDisplayName,
+                canChangeAvatar = entity.canChangeAvatar,
+                canChange3pid = entity.canChange3pid,
                 maxUploadFileSize = entity.maxUploadFileSize,
                 lastVersionIdentityServerSupported = entity.lastVersionIdentityServerSupported,
                 defaultIdentityServerUrl = entity.defaultIdentityServerUrl,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt
index 5413dd3d71c661b57b8c35f32ae5c3d3204bc0b3..f3770e4afee96aedb217207b66d4010dadf2a142 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt
@@ -19,23 +19,26 @@ package org.matrix.android.sdk.internal.database.mapper
 import org.matrix.android.sdk.api.session.room.model.ReadReceipt
 import org.matrix.android.sdk.internal.database.RealmSessionProvider
 import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity
-import org.matrix.android.sdk.internal.database.model.UserEntity
+import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
 import org.matrix.android.sdk.internal.database.query.where
 import javax.inject.Inject
 
-internal class ReadReceiptsSummaryMapper @Inject constructor(private val realmSessionProvider: RealmSessionProvider) {
+internal class ReadReceiptsSummaryMapper @Inject constructor(
+        private val realmSessionProvider: RealmSessionProvider
+) {
 
     fun map(readReceiptsSummaryEntity: ReadReceiptsSummaryEntity?): List<ReadReceipt> {
         if (readReceiptsSummaryEntity == null) {
             return emptyList()
         }
+        val readReceipts = readReceiptsSummaryEntity.readReceipts
+
         return realmSessionProvider.withRealm { realm ->
-            val readReceipts = readReceiptsSummaryEntity.readReceipts
             readReceipts
                     .mapNotNull {
-                        val user = UserEntity.where(realm, it.userId).findFirst()
+                        val roomMember = RoomMemberSummaryEntity.where(realm, roomId = it.roomId, userId = it.userId).findFirst()
                                 ?: return@mapNotNull null
-                        ReadReceipt(user.asDomain(), it.originServerTs.toLong())
+                        ReadReceipt(roomMember.asDomain(), it.originServerTs.toLong())
                     }
         }
     }
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 f3bea68c26e6bb1fc83cc7d6e56b1bda03b1a6b1..55c7f2a8ee605a7283e54bf0d66622275ae3e4c4 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
@@ -48,7 +48,7 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS
                 ),
                 readReceipts = readReceipts
                         ?.distinctBy {
-                            it.user
+                            it.roomMember
                         }?.sortedByDescending {
                             it.originServerTs
                         }.orEmpty()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo001.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo001.kt
new file mode 100644
index 0000000000000000000000000000000000000000..831c6280ad5607a964342c93321d5b14e57a9901
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo001.kt
@@ -0,0 +1,33 @@
+/*
+ * 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 org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+class MigrateSessionTo001(realm: DynamicRealm) : RealmMigrator(realm, 1) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        // Add hasFailedSending in RoomSummary and a small warning icon on room list
+        realm.schema.get("RoomSummaryEntity")
+                ?.addField(RoomSummaryEntityFields.HAS_FAILED_SENDING, Boolean::class.java)
+                ?.transform { obj ->
+                    obj.setBoolean(RoomSummaryEntityFields.HAS_FAILED_SENDING, false)
+                }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo002.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo002.kt
new file mode 100644
index 0000000000000000000000000000000000000000..215e558e2af40432c6e8b3ad80d93c18ae5fee85
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo002.kt
@@ -0,0 +1,31 @@
+/*
+ * 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 org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+class MigrateSessionTo002(realm: DynamicRealm) : RealmMigrator(realm, 2) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.get("HomeServerCapabilitiesEntity")
+                ?.addField("adminE2EByDefault", Boolean::class.java)
+                ?.transform { obj ->
+                    obj.setBoolean("adminE2EByDefault", true)
+                }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo003.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo003.kt
new file mode 100644
index 0000000000000000000000000000000000000000..bc0b79d7e640814c4e7f8ca0ababc5f4196da4e1
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo003.kt
@@ -0,0 +1,30 @@
+/*
+ * 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 org.matrix.android.sdk.internal.extensions.forceRefreshOfHomeServerCapabilities
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+class MigrateSessionTo003(realm: DynamicRealm) : RealmMigrator(realm, 3) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.get("HomeServerCapabilitiesEntity")
+                ?.addField("preferredJitsiDomain", String::class.java)
+                ?.forceRefreshOfHomeServerCapabilities()
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo004.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo004.kt
new file mode 100644
index 0000000000000000000000000000000000000000..be13ae2c2f6386dbc062977af87b3cd7d7ae803c
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo004.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 org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+class MigrateSessionTo004(realm: DynamicRealm) : RealmMigrator(realm, 4) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.create("PendingThreePidEntity")
+                .addField(PendingThreePidEntityFields.CLIENT_SECRET, String::class.java)
+                .setRequired(PendingThreePidEntityFields.CLIENT_SECRET, true)
+                .addField(PendingThreePidEntityFields.EMAIL, String::class.java)
+                .addField(PendingThreePidEntityFields.MSISDN, String::class.java)
+                .addField(PendingThreePidEntityFields.SEND_ATTEMPT, Int::class.java)
+                .addField(PendingThreePidEntityFields.SID, String::class.java)
+                .setRequired(PendingThreePidEntityFields.SID, true)
+                .addField(PendingThreePidEntityFields.SUBMIT_URL, String::class.java)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo005.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo005.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b4826b23a450dced5acd28612e5d92f5b0ec3de2
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo005.kt
@@ -0,0 +1,29 @@
+/*
+ * 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 org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+class MigrateSessionTo005(realm: DynamicRealm) : RealmMigrator(realm, 5) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.get("HomeServerCapabilitiesEntity")
+                ?.removeField("adminE2EByDefault")
+                ?.removeField("preferredJitsiDomain")
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo006.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo006.kt
new file mode 100644
index 0000000000000000000000000000000000000000..3d7f26cceeee627ea2bcee56b073acabb6a63dfb
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo006.kt
@@ -0,0 +1,37 @@
+/*
+ * 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 org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+class MigrateSessionTo006(realm: DynamicRealm) : RealmMigrator(realm, 6) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.create("PreviewUrlCacheEntity")
+                .addField(PreviewUrlCacheEntityFields.URL, String::class.java)
+                .setRequired(PreviewUrlCacheEntityFields.URL, true)
+                .addPrimaryKey(PreviewUrlCacheEntityFields.URL)
+                .addField(PreviewUrlCacheEntityFields.URL_FROM_SERVER, String::class.java)
+                .addField(PreviewUrlCacheEntityFields.SITE_NAME, String::class.java)
+                .addField(PreviewUrlCacheEntityFields.TITLE, String::class.java)
+                .addField(PreviewUrlCacheEntityFields.DESCRIPTION, String::class.java)
+                .addField(PreviewUrlCacheEntityFields.MXC_URL, String::class.java)
+                .addField(PreviewUrlCacheEntityFields.LAST_UPDATED_TIMESTAMP, Long::class.java)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo007.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo007.kt
new file mode 100644
index 0000000000000000000000000000000000000000..be8c8ce9c6d9ccc1331c09132513ab2352200d6e
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo007.kt
@@ -0,0 +1,38 @@
+/*
+ * 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 org.matrix.android.sdk.internal.database.model.RoomEntityFields
+import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+class MigrateSessionTo007(realm: DynamicRealm) : RealmMigrator(realm, 7) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.get("RoomEntity")
+                ?.addField(RoomEntityFields.MEMBERS_LOAD_STATUS_STR, String::class.java)
+                ?.transform { obj ->
+                    if (obj.getBoolean("areAllMembersLoaded")) {
+                        obj.setString("membersLoadStatusStr", RoomMembersLoadStatusType.LOADED.name)
+                    } else {
+                        obj.setString("membersLoadStatusStr", RoomMembersLoadStatusType.NONE.name)
+                    }
+                }
+                ?.removeField("areAllMembersLoaded")
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo008.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo008.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d46730ef701165515e4066c624997301ff0ce8dc
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo008.kt
@@ -0,0 +1,47 @@
+/*
+ * 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 org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntityFields
+import org.matrix.android.sdk.internal.database.model.EditionOfEventFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+class MigrateSessionTo008(realm: DynamicRealm) : RealmMigrator(realm, 8) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        val editionOfEventSchema = realm.schema.create("EditionOfEvent")
+                .addField(EditionOfEventFields.CONTENT, String::class.java)
+                .addField(EditionOfEventFields.EVENT_ID, String::class.java)
+                .setRequired(EditionOfEventFields.EVENT_ID, true)
+                .addField(EditionOfEventFields.SENDER_ID, String::class.java)
+                .setRequired(EditionOfEventFields.SENDER_ID, true)
+                .addField(EditionOfEventFields.TIMESTAMP, Long::class.java)
+                .addField(EditionOfEventFields.IS_LOCAL_ECHO, Boolean::class.java)
+
+        realm.schema.get("EditAggregatedSummaryEntity")
+                ?.removeField("aggregatedContent")
+                ?.removeField("sourceEvents")
+                ?.removeField("lastEditTs")
+                ?.removeField("sourceLocalEchoEvents")
+                ?.addRealmListField(EditAggregatedSummaryEntityFields.EDITIONS.`$`, editionOfEventSchema)
+
+        // This has to be done once a parent use the model as a child
+        // See https://github.com/realm/realm-java/issues/7402
+        editionOfEventSchema.isEmbedded = true
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo009.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo009.kt
new file mode 100644
index 0000000000000000000000000000000000000000..370430b9e3274daa5548d423a26dfab0bcc8177f
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo009.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.api.session.room.model.tag.RoomTag
+import org.matrix.android.sdk.internal.database.model.EventEntityFields
+import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
+import org.matrix.android.sdk.internal.database.model.RoomTagEntityFields
+import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+class MigrateSessionTo009(realm: DynamicRealm) : RealmMigrator(realm, 9) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.get("RoomSummaryEntity")
+                ?.addField(RoomSummaryEntityFields.LAST_ACTIVITY_TIME, Long::class.java, FieldAttribute.INDEXED)
+                ?.setNullable(RoomSummaryEntityFields.LAST_ACTIVITY_TIME, true)
+                ?.addIndex(RoomSummaryEntityFields.MEMBERSHIP_STR)
+                ?.addIndex(RoomSummaryEntityFields.IS_DIRECT)
+                ?.addIndex(RoomSummaryEntityFields.VERSIONING_STATE_STR)
+
+                ?.addField(RoomSummaryEntityFields.IS_FAVOURITE, Boolean::class.java)
+                ?.addIndex(RoomSummaryEntityFields.IS_FAVOURITE)
+                ?.addField(RoomSummaryEntityFields.IS_LOW_PRIORITY, Boolean::class.java)
+                ?.addIndex(RoomSummaryEntityFields.IS_LOW_PRIORITY)
+                ?.addField(RoomSummaryEntityFields.IS_SERVER_NOTICE, Boolean::class.java)
+                ?.addIndex(RoomSummaryEntityFields.IS_SERVER_NOTICE)
+
+                ?.transform { obj ->
+                    val isFavorite = obj.getList(RoomSummaryEntityFields.TAGS.`$`).any {
+                        it.getString(RoomTagEntityFields.TAG_NAME) == RoomTag.ROOM_TAG_FAVOURITE
+                    }
+                    obj.setBoolean(RoomSummaryEntityFields.IS_FAVOURITE, isFavorite)
+
+                    val isLowPriority = obj.getList(RoomSummaryEntityFields.TAGS.`$`).any {
+                        it.getString(RoomTagEntityFields.TAG_NAME) == RoomTag.ROOM_TAG_LOW_PRIORITY
+                    }
+
+                    obj.setBoolean(RoomSummaryEntityFields.IS_LOW_PRIORITY, isLowPriority)
+
+//                    XXX migrate last message origin server ts
+                    obj.getObject(RoomSummaryEntityFields.LATEST_PREVIEWABLE_EVENT.`$`)
+                            ?.getObject(TimelineEventEntityFields.ROOT.`$`)
+                            ?.getLong(EventEntityFields.ORIGIN_SERVER_TS)?.let {
+                                obj.setLong(RoomSummaryEntityFields.LAST_ACTIVITY_TIME, it)
+                            }
+                }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo010.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo010.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b968862d1096090a719f0f56705f3871bb654b04
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo010.kt
@@ -0,0 +1,70 @@
+/*
+ * 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 org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
+import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields
+import org.matrix.android.sdk.internal.database.model.EventEntityFields
+import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
+import org.matrix.android.sdk.internal.database.model.SpaceChildSummaryEntityFields
+import org.matrix.android.sdk.internal.database.model.SpaceParentSummaryEntityFields
+import org.matrix.android.sdk.internal.di.MoshiProvider
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+class MigrateSessionTo010(realm: DynamicRealm) : RealmMigrator(realm, 10) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.create("SpaceChildSummaryEntity")
+                ?.addField(SpaceChildSummaryEntityFields.ORDER, String::class.java)
+                ?.addField(SpaceChildSummaryEntityFields.CHILD_ROOM_ID, String::class.java)
+                ?.addField(SpaceChildSummaryEntityFields.AUTO_JOIN, Boolean::class.java)
+                ?.setNullable(SpaceChildSummaryEntityFields.AUTO_JOIN, true)
+                ?.addRealmObjectField(SpaceChildSummaryEntityFields.CHILD_SUMMARY_ENTITY.`$`, realm.schema.get("RoomSummaryEntity")!!)
+                ?.addRealmListField(SpaceChildSummaryEntityFields.VIA_SERVERS.`$`, String::class.java)
+
+        realm.schema.create("SpaceParentSummaryEntity")
+                ?.addField(SpaceParentSummaryEntityFields.PARENT_ROOM_ID, String::class.java)
+                ?.addField(SpaceParentSummaryEntityFields.CANONICAL, Boolean::class.java)
+                ?.setNullable(SpaceParentSummaryEntityFields.CANONICAL, true)
+                ?.addRealmObjectField(SpaceParentSummaryEntityFields.PARENT_SUMMARY_ENTITY.`$`, realm.schema.get("RoomSummaryEntity")!!)
+                ?.addRealmListField(SpaceParentSummaryEntityFields.VIA_SERVERS.`$`, String::class.java)
+
+        val creationContentAdapter = MoshiProvider.providesMoshi().adapter(RoomCreateContent::class.java)
+        realm.schema.get("RoomSummaryEntity")
+                ?.addField(RoomSummaryEntityFields.ROOM_TYPE, String::class.java)
+                ?.addField(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, String::class.java)
+                ?.addField(RoomSummaryEntityFields.GROUP_IDS, String::class.java)
+                ?.transform { obj ->
+
+                    val creationEvent = realm.where("CurrentStateEventEntity")
+                            .equalTo(CurrentStateEventEntityFields.ROOM_ID, obj.getString(RoomSummaryEntityFields.ROOM_ID))
+                            .equalTo(CurrentStateEventEntityFields.TYPE, EventType.STATE_ROOM_CREATE)
+                            .findFirst()
+
+                    val roomType = creationEvent?.getObject(CurrentStateEventEntityFields.ROOT.`$`)
+                            ?.getString(EventEntityFields.CONTENT)?.let {
+                                creationContentAdapter.fromJson(it)?.type
+                            }
+
+                    obj.setString(RoomSummaryEntityFields.ROOM_TYPE, roomType)
+                }
+                ?.addRealmListField(RoomSummaryEntityFields.PARENTS.`$`, realm.schema.get("SpaceParentSummaryEntity")!!)
+                ?.addRealmListField(RoomSummaryEntityFields.CHILDREN.`$`, realm.schema.get("SpaceChildSummaryEntity")!!)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo011.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo011.kt
new file mode 100644
index 0000000000000000000000000000000000000000..92ee26df42854a26c7ad46733381189a1eb43b39
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo011.kt
@@ -0,0 +1,29 @@
+/*
+ * 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 org.matrix.android.sdk.internal.database.model.EventEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+class MigrateSessionTo011(realm: DynamicRealm) : RealmMigrator(realm, 11) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.get("EventEntity")
+                ?.addField(EventEntityFields.SEND_STATE_DETAILS, String::class.java)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo012.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo012.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a914cadd80634ab2cfa4e12b5d0c812678beb37e
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo012.kt
@@ -0,0 +1,53 @@
+/*
+ * 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 org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
+import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields
+import org.matrix.android.sdk.internal.database.model.EventEntityFields
+import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
+import org.matrix.android.sdk.internal.database.model.SpaceChildSummaryEntityFields
+import org.matrix.android.sdk.internal.di.MoshiProvider
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+class MigrateSessionTo012(realm: DynamicRealm) : RealmMigrator(realm, 12) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        val joinRulesContentAdapter = MoshiProvider.providesMoshi().adapter(RoomJoinRulesContent::class.java)
+        realm.schema.get("RoomSummaryEntity")
+                ?.addField(RoomSummaryEntityFields.JOIN_RULES_STR, String::class.java)
+                ?.transform { obj ->
+                    val joinRulesEvent = realm.where("CurrentStateEventEntity")
+                            .equalTo(CurrentStateEventEntityFields.ROOM_ID, obj.getString(RoomSummaryEntityFields.ROOM_ID))
+                            .equalTo(CurrentStateEventEntityFields.TYPE, EventType.STATE_ROOM_JOIN_RULES)
+                            .findFirst()
+
+                    val roomJoinRules = joinRulesEvent?.getObject(CurrentStateEventEntityFields.ROOT.`$`)
+                            ?.getString(EventEntityFields.CONTENT)?.let {
+                                joinRulesContentAdapter.fromJson(it)?.joinRules
+                            }
+
+                    obj.setString(RoomSummaryEntityFields.JOIN_RULES_STR, roomJoinRules?.name)
+                }
+
+        realm.schema.get("SpaceChildSummaryEntity")
+                ?.addField(SpaceChildSummaryEntityFields.SUGGESTED, Boolean::class.java)
+                ?.setNullable(SpaceChildSummaryEntityFields.SUGGESTED, true)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo013.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo013.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2ea030380227d7cc9800be0483954ed0de50bef2
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo013.kt
@@ -0,0 +1,32 @@
+/*
+ * 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 org.matrix.android.sdk.internal.database.model.SpaceChildSummaryEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+class MigrateSessionTo013(realm: DynamicRealm) : RealmMigrator(realm, 13) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        // Fix issue with the nightly build. Eventually play again the migration which has been included in migrateTo12()
+        realm.schema.get("SpaceChildSummaryEntity")
+                ?.takeIf { !it.hasField(SpaceChildSummaryEntityFields.SUGGESTED) }
+                ?.addField(SpaceChildSummaryEntityFields.SUGGESTED, Boolean::class.java)
+                ?.setNullable(SpaceChildSummaryEntityFields.SUGGESTED, true)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo014.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo014.kt
new file mode 100644
index 0000000000000000000000000000000000000000..c524b6f2849d5e8ccd6c24d0051f63a03169c22a
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo014.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.api.session.room.model.VersioningState
+import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntityFields
+import org.matrix.android.sdk.internal.database.model.RoomEntityFields
+import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+class MigrateSessionTo014(realm: DynamicRealm) : RealmMigrator(realm, 14) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        val roomAccountDataSchema = realm.schema.create("RoomAccountDataEntity")
+                .addField(RoomAccountDataEntityFields.CONTENT_STR, String::class.java)
+                .addField(RoomAccountDataEntityFields.TYPE, String::class.java, FieldAttribute.INDEXED)
+
+        realm.schema.get("RoomEntity")
+                ?.addRealmListField(RoomEntityFields.ACCOUNT_DATA.`$`, roomAccountDataSchema)
+
+        realm.schema.get("RoomSummaryEntity")
+                ?.addField(RoomSummaryEntityFields.IS_HIDDEN_FROM_USER, Boolean::class.java, FieldAttribute.INDEXED)
+                ?.transform {
+                    val isHiddenFromUser = it.getString(RoomSummaryEntityFields.VERSIONING_STATE_STR) == VersioningState.UPGRADED_ROOM_JOINED.name
+                    it.setBoolean(RoomSummaryEntityFields.IS_HIDDEN_FROM_USER, isHiddenFromUser)
+                }
+
+        roomAccountDataSchema.isEmbedded = true
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo015.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo015.kt
new file mode 100644
index 0000000000000000000000000000000000000000..329964a9a406c597ceb3f8c6a4fc4312d288d2ca
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo015.kt
@@ -0,0 +1,38 @@
+/*
+ * 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 org.matrix.android.sdk.api.session.room.model.Membership
+import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
+import org.matrix.android.sdk.internal.query.process
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+class MigrateSessionTo015(realm: DynamicRealm) : RealmMigrator(realm, 15) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        // fix issue with flattenParentIds on DM that kept growing with duplicate
+        // so we reset it, will be updated next sync
+        realm.where("RoomSummaryEntity")
+                .process(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.activeMemberships())
+                .equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
+                .findAll()
+                .onEach {
+                    it.setString(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, null)
+                }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo016.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo016.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b2fa54a05c30c6fce2ba02bef44420e2d612898f
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo016.kt
@@ -0,0 +1,31 @@
+/*
+ * 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 org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
+import org.matrix.android.sdk.internal.extensions.forceRefreshOfHomeServerCapabilities
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+class MigrateSessionTo016(realm: DynamicRealm) : RealmMigrator(realm, 16) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.get("HomeServerCapabilitiesEntity")
+                ?.addField(HomeServerCapabilitiesEntityFields.ROOM_VERSIONS_JSON, String::class.java)
+                ?.forceRefreshOfHomeServerCapabilities()
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo017.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo017.kt
new file mode 100644
index 0000000000000000000000000000000000000000..95d67b9ad842b23d5b6e4108706aa16900398365
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo017.kt
@@ -0,0 +1,29 @@
+/*
+ * 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 org.matrix.android.sdk.internal.database.model.EventInsertEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+class MigrateSessionTo017(realm: DynamicRealm) : RealmMigrator(realm, 17) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.get("EventInsertEntity")
+                ?.addField(EventInsertEntityFields.CAN_BE_PROCESSED, Boolean::class.java)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo018.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo018.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b415c51d4b53e9fb7ca78b54b657d5160ffc2a7b
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo018.kt
@@ -0,0 +1,48 @@
+/*
+ * 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 org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
+import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
+import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+class MigrateSessionTo018(realm: DynamicRealm) : RealmMigrator(realm, 18) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.create("UserPresenceEntity")
+                ?.addField(UserPresenceEntityFields.USER_ID, String::class.java)
+                ?.addPrimaryKey(UserPresenceEntityFields.USER_ID)
+                ?.setRequired(UserPresenceEntityFields.USER_ID, true)
+                ?.addField(UserPresenceEntityFields.PRESENCE_STR, String::class.java)
+                ?.addField(UserPresenceEntityFields.LAST_ACTIVE_AGO, Long::class.java)
+                ?.setNullable(UserPresenceEntityFields.LAST_ACTIVE_AGO, true)
+                ?.addField(UserPresenceEntityFields.STATUS_MESSAGE, String::class.java)
+                ?.addField(UserPresenceEntityFields.IS_CURRENTLY_ACTIVE, Boolean::class.java)
+                ?.setNullable(UserPresenceEntityFields.IS_CURRENTLY_ACTIVE, true)
+                ?.addField(UserPresenceEntityFields.AVATAR_URL, String::class.java)
+                ?.addField(UserPresenceEntityFields.DISPLAY_NAME, String::class.java)
+
+        val userPresenceEntity = realm.schema.get("UserPresenceEntity") ?: return
+        realm.schema.get("RoomSummaryEntity")
+                ?.addRealmObjectField(RoomSummaryEntityFields.DIRECT_USER_PRESENCE.`$`, userPresenceEntity)
+
+        realm.schema.get("RoomMemberSummaryEntity")
+                ?.addRealmObjectField(RoomMemberSummaryEntityFields.USER_PRESENCE_ENTITY.`$`, userPresenceEntity)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo019.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo019.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d0b368be46e95c0ff082ce54036145afb965fcb1
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo019.kt
@@ -0,0 +1,37 @@
+/*
+ * 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 org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
+import org.matrix.android.sdk.internal.util.Normalizer
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+class MigrateSessionTo019(realm: DynamicRealm,
+                          private val normalizer: Normalizer) : RealmMigrator(realm, 19) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.get("RoomSummaryEntity")
+                ?.addField(RoomSummaryEntityFields.NORMALIZED_DISPLAY_NAME, String::class.java)
+                ?.transform {
+                    it.getString(RoomSummaryEntityFields.DISPLAY_NAME)?.let { displayName ->
+                        val normalised = normalizer.normalize(displayName)
+                        it.set(RoomSummaryEntityFields.NORMALIZED_DISPLAY_NAME, normalised)
+                    }
+                }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo020.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo020.kt
new file mode 100644
index 0000000000000000000000000000000000000000..c7f6e3ceed4c7046283eaf10769252ca54f6e3cd
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo020.kt
@@ -0,0 +1,45 @@
+/*
+ * 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 org.matrix.android.sdk.internal.database.model.ChunkEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+class MigrateSessionTo020(realm: DynamicRealm) : RealmMigrator(realm, 20) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.get("ChunkEntity")?.apply {
+            if (hasField("numberOfTimelineEvents")) {
+                removeField("numberOfTimelineEvents")
+            }
+            var cleanOldChunks = false
+            if (!hasField(ChunkEntityFields.NEXT_CHUNK.`$`)) {
+                cleanOldChunks = true
+                addRealmObjectField(ChunkEntityFields.NEXT_CHUNK.`$`, this)
+            }
+            if (!hasField(ChunkEntityFields.PREV_CHUNK.`$`)) {
+                cleanOldChunks = true
+                addRealmObjectField(ChunkEntityFields.PREV_CHUNK.`$`, this)
+            }
+            if (cleanOldChunks) {
+                val chunkEntities = realm.where("ChunkEntity").equalTo(ChunkEntityFields.IS_LAST_FORWARD, false).findAll()
+                chunkEntities.deleteAllFromRealm()
+            }
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo021.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo021.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6b6952e69784b895e0d4e8a3c83aa07d7e8837b0
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo021.kt
@@ -0,0 +1,55 @@
+/*
+ * 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 org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.internal.crypto.model.event.EncryptionEventContent
+import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields
+import org.matrix.android.sdk.internal.database.model.EventEntityFields
+import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
+import org.matrix.android.sdk.internal.di.MoshiProvider
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+class MigrateSessionTo021(realm: DynamicRealm) : RealmMigrator(realm, 21) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.get("RoomSummaryEntity")
+                ?.addField(RoomSummaryEntityFields.E2E_ALGORITHM, String::class.java)
+                ?.transform { obj ->
+
+                    val encryptionContentAdapter = MoshiProvider.providesMoshi().adapter(EncryptionEventContent::class.java)
+
+                    val encryptionEvent = realm.where("CurrentStateEventEntity")
+                            .equalTo(CurrentStateEventEntityFields.ROOM_ID, obj.getString(RoomSummaryEntityFields.ROOM_ID))
+                            .equalTo(CurrentStateEventEntityFields.TYPE, EventType.STATE_ROOM_ENCRYPTION)
+                            .findFirst()
+
+                    val encryptionEventRoot = encryptionEvent?.getObject(CurrentStateEventEntityFields.ROOT.`$`)
+                    val algorithm = encryptionEventRoot
+                            ?.getString(EventEntityFields.CONTENT)?.let {
+                                encryptionContentAdapter.fromJson(it)?.algorithm
+                            }
+
+                    obj.setString(RoomSummaryEntityFields.E2E_ALGORITHM, algorithm)
+                    obj.setBoolean(RoomSummaryEntityFields.IS_ENCRYPTED, encryptionEvent != null)
+                    encryptionEventRoot?.getLong(EventEntityFields.ORIGIN_SERVER_TS)?.let {
+                        obj.setLong(RoomSummaryEntityFields.ENCRYPTION_EVENT_TS, it)
+                    }
+                }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo022.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo022.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e78a9d05da2996d09e2bd7ffbd7193d2a4b0a494
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo022.kt
@@ -0,0 +1,42 @@
+/*
+ * 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 org.matrix.android.sdk.api.session.room.model.Membership
+import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields
+import org.matrix.android.sdk.internal.database.model.RoomEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+import timber.log.Timber
+
+class MigrateSessionTo022(realm: DynamicRealm) : RealmMigrator(realm, 22) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        val listJoinedRoomIds = realm.where("RoomEntity")
+                .equalTo(RoomEntityFields.MEMBERSHIP_STR, Membership.JOIN.name).findAll()
+                .map { it.getString(RoomEntityFields.ROOM_ID) }
+
+        val hasMissingStateEvent = realm.where("CurrentStateEventEntity")
+                .`in`(CurrentStateEventEntityFields.ROOM_ID, listJoinedRoomIds.toTypedArray())
+                .isNull(CurrentStateEventEntityFields.ROOT.`$`).findFirst() != null
+
+        if (hasMissingStateEvent) {
+            Timber.v("Has some missing state event, clear session cache")
+            realm.deleteAll()
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo023.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo023.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0bb8ceeaa5eef8cf0cfbfb461b112cad9571ba72
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo023.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.api.session.threads.ThreadNotificationState
+import org.matrix.android.sdk.internal.database.model.EventEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+class MigrateSessionTo023(realm: DynamicRealm) : RealmMigrator(realm, 23) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        val eventEntity = realm.schema.get("TimelineEventEntity") ?: return
+
+        realm.schema.get("EventEntity")
+                ?.addField(EventEntityFields.IS_ROOT_THREAD, Boolean::class.java, FieldAttribute.INDEXED)
+                ?.addField(EventEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED)
+                ?.addField(EventEntityFields.NUMBER_OF_THREADS, Int::class.java)
+                ?.addField(EventEntityFields.THREAD_NOTIFICATION_STATE_STR, String::class.java)
+                ?.transform {
+                    it.setString(EventEntityFields.THREAD_NOTIFICATION_STATE_STR, ThreadNotificationState.NO_NEW_MESSAGE.name)
+                }
+                ?.addRealmObjectField(EventEntityFields.THREAD_SUMMARY_LATEST_MESSAGE.`$`, eventEntity)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo024.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo024.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ff8897256691098dbf8dcef71843c9d108102187
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo024.kt
@@ -0,0 +1,32 @@
+/*
+ * 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 org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+class MigrateSessionTo024(realm: DynamicRealm) : RealmMigrator(realm, 24) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.get("PreviewUrlCacheEntity")
+                ?.addField(PreviewUrlCacheEntityFields.IMAGE_WIDTH, Int::class.java)
+                ?.setNullable(PreviewUrlCacheEntityFields.IMAGE_WIDTH, true)
+                ?.addField(PreviewUrlCacheEntityFields.IMAGE_HEIGHT, Int::class.java)
+                ?.setNullable(PreviewUrlCacheEntityFields.IMAGE_HEIGHT, true)
+    }
+}
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 0000000000000000000000000000000000000000..237b016ac254ed5bdcedff959dda77a45c45210e
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo025.kt
@@ -0,0 +1,33 @@
+/*
+ * 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 org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
+import org.matrix.android.sdk.internal.extensions.forceRefreshOfHomeServerCapabilities
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+class MigrateSessionTo025(realm: DynamicRealm) : RealmMigrator(realm, 25) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.get("HomeServerCapabilitiesEntity")
+                ?.addField(HomeServerCapabilitiesEntityFields.CAN_CHANGE_DISPLAY_NAME, Boolean::class.java)
+                ?.addField(HomeServerCapabilitiesEntityFields.CAN_CHANGE_AVATAR, Boolean::class.java)
+                ?.addField(HomeServerCapabilitiesEntityFields.CAN_CHANGE3PID, Boolean::class.java)
+                ?.forceRefreshOfHomeServerCapabilities()
+    }
+}
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 ce2d1efc1d21d17a5c8f213d5640171a10e9d630..445181e576416c50a287701c98cf6d2efb8bf1c9 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt
@@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.database.model
 import io.realm.RealmObject
 import io.realm.annotations.Index
 import org.matrix.android.sdk.api.session.room.send.SendState
-import org.matrix.android.sdk.api.util.JsonDict
+import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
 import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult
 import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
 import org.matrix.android.sdk.internal.di.MoshiProvider
@@ -40,7 +40,12 @@ internal open class EventEntity(@Index var eventId: String = "",
                                 var unsignedData: String? = null,
                                 var redacts: String? = null,
                                 var decryptionResultJson: String? = null,
-                                var ageLocalTs: Long? = null
+                                var ageLocalTs: Long? = null,
+                                // 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,
+                                var threadSummaryLatestMessage: TimelineEventEntity? = null
 ) : RealmObject() {
 
     private var sendStateStr: String = SendState.UNKNOWN.name
@@ -53,6 +58,15 @@ internal open class EventEntity(@Index var eventId: String = "",
             sendStateStr = value.name
         }
 
+    private var threadNotificationStateStr: String = ThreadNotificationState.NO_NEW_MESSAGE.name
+    var threadNotificationState: ThreadNotificationState
+        get() {
+            return ThreadNotificationState.valueOf(threadNotificationStateStr)
+        }
+        set(value) {
+            threadNotificationStateStr = value.name
+        }
+
     var decryptionErrorCode: String? = null
         set(value) {
             if (value != field) field = value
@@ -65,10 +79,10 @@ internal open class EventEntity(@Index var eventId: String = "",
 
     companion object
 
-    fun setDecryptionResult(result: MXEventDecryptionResult, clearEvent: JsonDict? = null) {
+    fun setDecryptionResult(result: MXEventDecryptionResult) {
         assertIsManaged()
         val decryptionResult = OlmDecryptionResult(
-                payload = clearEvent ?: result.clearEvent,
+                payload = result.clearEvent,
                 senderKey = result.senderCurve25519Key,
                 keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
                 forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
@@ -84,4 +98,6 @@ internal open class EventEntity(@Index var eventId: String = "",
                 .findFirst()
                 ?.canBeProcessed = true
     }
+
+    fun isThread(): Boolean = rootThreadEventId != null
 }
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 980449ddfbe4235c60044370ea6c43da42eac691..08ecd5995ec490c8a14cfc35f1186b35740e2f9e 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
@@ -21,6 +21,9 @@ import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
 
 internal open class HomeServerCapabilitiesEntity(
         var canChangePassword: Boolean = true,
+        var canChangeDisplayName: Boolean = true,
+        var canChangeAvatar: Boolean = true,
+        var canChange3pid: Boolean = true,
         var roomVersionsJson: String? = null,
         var maxUploadFileSize: Long = HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN,
         var lastVersionIdentityServerSupported: Boolean = false,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PreviewUrlCacheEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PreviewUrlCacheEntity.kt
index b1e0b644056d36158de4acb21aaeb9f056da2974..f19d70a1f28a525e15dab6b776efb583f6cf90bb 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PreviewUrlCacheEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PreviewUrlCacheEntity.kt
@@ -28,7 +28,8 @@ internal open class PreviewUrlCacheEntity(
         var title: String? = null,
         var description: String? = null,
         var mxcUrl: String? = null,
-
+        var imageWidth: Int? = null,
+        var imageHeight: Int? = null,
         var lastUpdatedTimestamp: Long = 0L
 ) : RealmObject() {
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt
index 240b2a069114de2d73590465e76f663bc0cd1271..f7fa1037ba59687e450c5667cf5b2dcc3a714319 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt
@@ -49,6 +49,11 @@ internal fun EventEntity.Companion.where(realm: Realm, eventId: String): RealmQu
             .equalTo(EventEntityFields.EVENT_ID, eventId)
 }
 
+internal fun EventEntity.Companion.whereRoomId(realm: Realm, roomId: String): RealmQuery<EventEntity> {
+    return realm.where<EventEntity>()
+            .equalTo(EventEntityFields.ROOM_ID, roomId)
+}
+
 internal fun EventEntity.Companion.where(realm: Realm, eventIds: List<String>): RealmQuery<EventEntity> {
     return realm.where<EventEntity>()
             .`in`(EventEntityFields.EVENT_ID, eventIds.toTypedArray())
@@ -85,3 +90,8 @@ internal fun RealmList<EventEntity>.find(eventId: String): EventEntity? {
 internal fun RealmList<EventEntity>.fastContains(eventId: String): Boolean {
     return this.find(eventId) != null
 }
+
+internal fun EventEntity.Companion.whereRootThreadEventId(realm: Realm, rootThreadEventId: String): RealmQuery<EventEntity> {
+    return realm.where<EventEntity>()
+            .equalTo(EventEntityFields.ROOT_THREAD_EVENT_ID, rootThreadEventId)
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt
index c9c96b9cc10b55213771fab7bf9d14cfc7ed22d3..8cc99c3d2fdf21c6705686e6bd812d7290c2beac 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt
@@ -34,27 +34,29 @@ internal fun isEventRead(realmConfiguration: RealmConfiguration,
     if (LocalEcho.isLocalEchoId(eventId)) {
         return true
     }
-    // If we don't know if the event has been read, we assume it's not
-    var isEventRead = false
 
-    Realm.getInstance(realmConfiguration).use { realm ->
-        val latestEvent = TimelineEventEntity.latestEvent(realm, roomId, true)
-        // If latest event is from you we are sure the event is read
-        if (latestEvent?.root?.sender == userId) {
-            return true
-        }
+    return Realm.getInstance(realmConfiguration).use { realm ->
         val eventToCheck = TimelineEventEntity.where(realm, roomId, eventId).findFirst()
-        isEventRead = when {
-            eventToCheck == null                -> false
-            eventToCheck.root?.sender == userId -> true
-            else                                -> {
-                val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst() ?: return@use
-                val readReceiptEvent = TimelineEventEntity.where(realm, roomId, readReceipt.eventId).findFirst() ?: return@use
-                readReceiptEvent.isMoreRecentThan(eventToCheck)
-            }
+        when {
+            // The event doesn't exist locally, let's assume it hasn't been read
+            eventToCheck == null                                          -> false
+            eventToCheck.root?.sender == userId                           -> true
+            // If new event exists and the latest event is from ourselves we can infer the event is read
+            latestEventIsFromSelf(realm, roomId, userId)                  -> true
+            eventToCheck.isBeforeLatestReadReceipt(realm, roomId, userId) -> true
+            else                                                          -> false
         }
     }
-    return isEventRead
+}
+
+private fun latestEventIsFromSelf(realm: Realm, roomId: String, userId: String) = TimelineEventEntity.latestEvent(realm, roomId, true)
+        ?.root?.sender == userId
+
+private fun TimelineEventEntity.isBeforeLatestReadReceipt(realm: Realm, roomId: String, userId: String): Boolean {
+    return ReadReceiptEntity.where(realm, roomId, userId).findFirst()?.let { readReceipt ->
+        val readReceiptEvent = TimelineEventEntity.where(realm, roomId, readReceipt.eventId).findFirst()
+        readReceiptEvent?.isMoreRecentThan(this)
+    } ?: false
 }
 
 /**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt
index aa1ce41bb7cfab7e9d54d61c591e759ba71f68af..63f41ebf2cf47473104e34192f759bf254b1e1fc 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt
@@ -59,6 +59,7 @@ internal fun TimelineEventEntity.Companion.latestEvent(realm: Realm,
                                                        filters: TimelineEventFilters = TimelineEventFilters()): TimelineEventEntity? {
     val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: return null
     val sendingTimelineEvents = roomEntity.sendingTimelineEvents.where().filterEvents(filters)
+
     val liveEvents = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)?.timelineEvents?.where()?.filterEvents(filters)
     val query = if (includesSending && sendingTimelineEvents.findAll().isNotEmpty()) {
         sendingTimelineEvents
@@ -100,6 +101,7 @@ internal fun RealmQuery<TimelineEventEntity>.filterEvents(filters: TimelineEvent
     if (filters.filterRedacted) {
         not().like(TimelineEventEntityFields.ROOT.UNSIGNED_DATA, TimelineEventFilter.Unsigned.REDACTED)
     }
+
     return this
 }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/RealmExtensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/RealmExtensions.kt
index e52e32e16ad0ea2f280478fc96b24fd43a425917..28b9f6418826886193330fd5e14ef4f5a4c8df52 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/RealmExtensions.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/RealmExtensions.kt
@@ -18,6 +18,8 @@ package org.matrix.android.sdk.internal.extensions
 
 import io.realm.RealmList
 import io.realm.RealmObject
+import io.realm.RealmObjectSchema
+import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
 
 internal fun RealmObject.assertIsManaged() {
     check(isManaged) { "${javaClass.simpleName} entity should be managed to use this function" }
@@ -31,3 +33,12 @@ internal fun <T> RealmList<T>.clearWith(delete: (T) -> Unit) {
         first()?.let { delete.invoke(it) }
     }
 }
+
+/**
+ * Schedule a refresh of the HomeServers capabilities
+ */
+internal fun RealmObjectSchema?.forceRefreshOfHomeServerCapabilities(): RealmObjectSchema? {
+    return this?.transform { obj ->
+        obj.setLong(HomeServerCapabilitiesEntityFields.LAST_UPDATED_TIMESTAMP, 0)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/DefaultLegacySessionImporter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/DefaultLegacySessionImporter.kt
index 445b6be8e87028a58cc2595282211d05764e7b26..22085e30fc0902009e77181a5919ac666ba27a10 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/DefaultLegacySessionImporter.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/DefaultLegacySessionImporter.kt
@@ -42,7 +42,8 @@ import org.matrix.android.sdk.internal.legacy.riot.HomeServerConnectionConfig as
 internal class DefaultLegacySessionImporter @Inject constructor(
         private val context: Context,
         private val sessionParamsStore: SessionParamsStore,
-        private val realmKeysUtils: RealmKeysUtils
+        private val realmKeysUtils: RealmKeysUtils,
+        private val realmCryptoStoreMigration: RealmCryptoStoreMigration
 ) : LegacySessionImporter {
 
     private val loginStorage = LoginStorage(context)
@@ -170,8 +171,8 @@ internal class DefaultLegacySessionImporter @Inject constructor(
                 .directory(File(context.filesDir, userMd5))
                 .name("crypto_store.realm")
                 .modules(RealmCryptoStoreModule())
-                .schemaVersion(RealmCryptoStoreMigration.CRYPTO_STORE_SCHEMA_VERSION)
-                .migration(RealmCryptoStoreMigration)
+                .schemaVersion(realmCryptoStoreMigration.schemaVersion)
+                .migration(realmCryptoStoreMigration)
                 .build()
 
         Timber.d("Migration: copy DB to encrypted DB")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GlobalRealmMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GlobalRealmMigration.kt
index 49bcc7218146008c97c05b74baeef8654959ea26..8dffac5fa074f1705a8c2ef62091af72bcc65641 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GlobalRealmMigration.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GlobalRealmMigration.kt
@@ -18,24 +18,23 @@ package org.matrix.android.sdk.internal.raw
 
 import io.realm.DynamicRealm
 import io.realm.RealmMigration
-import org.matrix.android.sdk.internal.database.model.KnownServerUrlEntityFields
+import org.matrix.android.sdk.internal.raw.migration.MigrateGlobalTo001
 import timber.log.Timber
+import javax.inject.Inject
 
-internal object GlobalRealmMigration : RealmMigration {
+internal class GlobalRealmMigration @Inject constructor() : RealmMigration {
+    /**
+     * Forces all GlobalRealmMigration instances to be equal
+     * Avoids Realm throwing when multiple instances of the migration are set
+     */
+    override fun equals(other: Any?) = other is GlobalRealmMigration
+    override fun hashCode() = 2000
 
-    // Current schema version
-    const val SCHEMA_VERSION = 1L
+    val schemaVersion = 1L
 
     override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
-        Timber.d("Migrating Auth Realm from $oldVersion to $newVersion")
+        Timber.d("Migrating Global Realm from $oldVersion to $newVersion")
 
-        if (oldVersion <= 0) migrateTo1(realm)
-    }
-
-    private fun migrateTo1(realm: DynamicRealm) {
-        realm.schema.create("KnownServerUrlEntity")
-                .addField(KnownServerUrlEntityFields.URL, String::class.java)
-                .addPrimaryKey(KnownServerUrlEntityFields.URL)
-                .setRequired(KnownServerUrlEntityFields.URL, true)
+        if (oldVersion < 1) MigrateGlobalTo001(realm).perform()
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/RawModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/RawModule.kt
index 50721b809a766644bf00a87f6c7567bef4569d6e..a8309766710007e1101b488f97be61035bcfce8c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/RawModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/RawModule.kt
@@ -51,14 +51,15 @@ internal abstract class RawModule {
         @Provides
         @GlobalDatabase
         @MatrixScope
-        fun providesRealmConfiguration(realmKeysUtils: RealmKeysUtils): RealmConfiguration {
+        fun providesRealmConfiguration(realmKeysUtils: RealmKeysUtils,
+                                       globalRealmMigration: GlobalRealmMigration): RealmConfiguration {
             return RealmConfiguration.Builder()
                     .apply {
                         realmKeysUtils.configureEncryption(this, DB_ALIAS)
                     }
                     .name("matrix-sdk-global.realm")
-                    .schemaVersion(GlobalRealmMigration.SCHEMA_VERSION)
-                    .migration(GlobalRealmMigration)
+                    .schemaVersion(globalRealmMigration.schemaVersion)
+                    .migration(globalRealmMigration)
                     .allowWritesOnUiThread(true)
                     .modules(GlobalRealmModule())
                     .build()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/migration/MigrateGlobalTo001.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/migration/MigrateGlobalTo001.kt
new file mode 100644
index 0000000000000000000000000000000000000000..cff2f7b8e86ba3610ea22febf8da95d1b9abd2aa
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/migration/MigrateGlobalTo001.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.raw.migration
+
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.internal.database.model.KnownServerUrlEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+class MigrateGlobalTo001(realm: DynamicRealm) : RealmMigrator(realm, 1) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.create("KnownServerUrlEntity")
+                .addField(KnownServerUrlEntityFields.URL, String::class.java)
+                .addPrimaryKey(KnownServerUrlEntityFields.URL)
+                .setRequired(KnownServerUrlEntityFields.URL, true)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cleanup/CleanupSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cleanup/CleanupSession.kt
index c42141a0aa2bfce08df6f2dd506f1c5ed3eeffc1..44fff45917df278e22d277bb03cc619bef9d86bf 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cleanup/CleanupSession.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cleanup/CleanupSession.kt
@@ -94,12 +94,12 @@ internal class CleanupSession @Inject constructor(
         do {
             val sessionRealmCount = Realm.getGlobalInstanceCount(realmSessionConfiguration)
             val cryptoRealmCount = Realm.getGlobalInstanceCount(realmCryptoConfiguration)
-            Timber.d("Wait for all Realm instance to be closed ($sessionRealmCount - $cryptoRealmCount)")
             if (sessionRealmCount > 0 || cryptoRealmCount > 0) {
-                Timber.d("Waiting ${TIME_TO_WAIT_MILLIS}ms")
+                Timber.d("Waiting ${TIME_TO_WAIT_MILLIS}ms for all Realm instance to be closed ($sessionRealmCount - $cryptoRealmCount)")
                 delay(TIME_TO_WAIT_MILLIS)
                 timeToWaitMillis -= TIME_TO_WAIT_MILLIS
             } else {
+                Timber.d("Finished waiting for all Realm instance to be closed ($sessionRealmCount - $cryptoRealmCount)")
                 timeToWaitMillis = 0
             }
         } while (timeToWaitMillis > 0)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt
index 82cd682eae0ecc50bd69db958e16f8685f1d9d03..55db64f309157b5a7c24848d0de96fea942637a0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt
@@ -66,7 +66,7 @@ internal class ThumbnailExtractor @Inject constructor(
                 thumbnail.recycle()
                 outputStream.reset()
             } ?: run {
-                Timber.e("Cannot extract video thumbnail at %s", attachment.queryUri.toString())
+                Timber.e("Cannot extract video thumbnail at ${attachment.queryUri}")
             }
         } catch (e: Exception) {
             Timber.e(e, "Cannot extract video thumbnail")
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 7047d38260e116ec3d79932435a67fd42e3ad0d2..f49832296708f98ff7743224048411e1cbce0b1f 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
@@ -48,6 +48,16 @@ data class RoomEventFilter(
          * a wildcard to match any sequence of characters.
          */
         @Json(name = "types") val types: List<String>? = null,
+        /**
+         * 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,
+        /**
+         *  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,
         /**
          * 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 b36d05b6c0881d0383749e31b38c9f241ba21f3e..830a58cd12813398d0df5efe6b1b55892947172a 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
@@ -18,7 +18,6 @@ package org.matrix.android.sdk.internal.session.homeserver
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
-import org.matrix.android.sdk.api.extensions.orTrue
 import org.matrix.android.sdk.api.util.JsonDict
 
 /**
@@ -37,10 +36,30 @@ internal data class GetCapabilitiesResult(
 internal data class Capabilities(
         /**
          * Capability to indicate if the user can change their password.
+         * True if the user can change their password, false otherwise.
          */
         @Json(name = "m.change_password")
-        val changePassword: ChangePassword? = null,
+        val changePassword: BooleanCapability? = null,
 
+        /**
+         * Capability to indicate if the user can change their display name.
+         * True if the user can change their display name, false otherwise.
+         */
+        @Json(name = "m.set_displayname")
+        val changeDisplayName: BooleanCapability? = null,
+
+        /**
+         * Capability to indicate if the user can change their avatar.
+         * True if the user can change their avatar, false otherwise.
+         */
+        @Json(name = "m.set_avatar_url")
+        val changeAvatar: BooleanCapability? = null,
+        /**
+         * Capability to indicate if the user can change add, remove or change 3PID associations.
+         * True if the user can change their 3PID associations, false otherwise.
+         */
+        @Json(name = "m.3pid_changes")
+        val change3pid: BooleanCapability? = null,
         /**
          * 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.
@@ -50,9 +69,9 @@ internal data class Capabilities(
 )
 
 @JsonClass(generateAdapter = true)
-internal data class ChangePassword(
+internal data class BooleanCapability(
         /**
-         * Required. True if the user can change their password, false otherwise.
+         * Required.
          */
         @Json(name = "enabled")
         val enabled: Boolean?
@@ -87,8 +106,3 @@ internal data class RoomVersions(
         @Json(name = "org.matrix.msc3244.room_capabilities")
         val roomCapabilities: JsonDict? = null
 )
-
-// The spec says: If not present, the client should assume that password changes are possible via the API
-internal fun GetCapabilitiesResult.canChangePassword(): Boolean {
-    return capabilities?.changePassword?.enabled.orTrue()
-}
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 612b98f8638d50cda4273328fea4d21a07061e17..e822cbdcdbf42c06fa2c282dff5b8189400cd8fd 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.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.isLoginAndRegistrationSupportedBySdk
@@ -108,9 +109,16 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
             val homeServerCapabilitiesEntity = HomeServerCapabilitiesEntity.getOrCreate(realm)
 
             if (getCapabilitiesResult != null) {
-                homeServerCapabilitiesEntity.canChangePassword = getCapabilitiesResult.canChangePassword()
+                val capabilities = getCapabilitiesResult.capabilities
 
-                homeServerCapabilitiesEntity.roomVersionsJson = getCapabilitiesResult.capabilities?.roomVersions?.let {
+                // The spec says: If not present, the client should assume that
+                // password, display name, avatar changes and 3pid changes are possible via the API
+                homeServerCapabilitiesEntity.canChangePassword = capabilities?.changePassword?.enabled.orTrue()
+                homeServerCapabilitiesEntity.canChangeDisplayName = capabilities?.changeDisplayName?.enabled.orTrue()
+                homeServerCapabilitiesEntity.canChangeAvatar = capabilities?.changeAvatar?.enabled.orTrue()
+                homeServerCapabilitiesEntity.canChange3pid = capabilities?.change3pid?.enabled.orTrue()
+
+                homeServerCapabilitiesEntity.roomVersionsJson = capabilities?.roomVersions?.let {
                     MoshiProvider.providesMoshi().adapter(RoomVersions::class.java).toJson(it)
                 }
             }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityModule.kt
index 65794e6b14a7a685fa2b00dcb2f2c3e2d6f6e84b..4e9d7dc7f764cad6d5cd0cee2265c6e3663d14e8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityModule.kt
@@ -60,6 +60,7 @@ internal abstract class IdentityModule {
         @IdentityDatabase
         @SessionScope
         fun providesIdentityRealmConfiguration(realmKeysUtils: RealmKeysUtils,
+                                               realmIdentityStoreMigration: RealmIdentityStoreMigration,
                                                @SessionFilesDirectory directory: File,
                                                @UserMd5 userMd5: String): RealmConfiguration {
             return RealmConfiguration.Builder()
@@ -68,8 +69,8 @@ internal abstract class IdentityModule {
                     .apply {
                         realmKeysUtils.configureEncryption(this, SessionModule.getKeyAlias(userMd5))
                     }
-                    .schemaVersion(RealmIdentityStoreMigration.IDENTITY_STORE_SCHEMA_VERSION)
-                    .migration(RealmIdentityStoreMigration)
+                    .schemaVersion(realmIdentityStoreMigration.schemaVersion)
+                    .migration(realmIdentityStoreMigration)
                     .allowWritesOnUiThread(true)
                     .modules(IdentityRealmModule())
                     .build()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/RealmIdentityStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/RealmIdentityStoreMigration.kt
index 21c0f8eb9e6cf64c8bb7af563ad7bced77157d67..0c279d8a7ed80f42e8bc1df22e55f1f82d31eb41 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/RealmIdentityStoreMigration.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/RealmIdentityStoreMigration.kt
@@ -18,23 +18,23 @@ package org.matrix.android.sdk.internal.session.identity.db
 
 import io.realm.DynamicRealm
 import io.realm.RealmMigration
+import org.matrix.android.sdk.internal.session.identity.db.migration.MigrateIdentityTo001
 import timber.log.Timber
+import javax.inject.Inject
 
-internal object RealmIdentityStoreMigration : RealmMigration {
+internal class RealmIdentityStoreMigration @Inject constructor() : RealmMigration {
+    /**
+     * Forces all RealmIdentityStoreMigration instances to be equal
+     * Avoids Realm throwing when multiple instances of the migration are set
+     */
+    override fun equals(other: Any?) = other is RealmIdentityStoreMigration
+    override fun hashCode() = 3000
 
-    const val IDENTITY_STORE_SCHEMA_VERSION = 1L
+    val schemaVersion = 1L
 
     override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
-        Timber.v("Migrating Realm Identity from $oldVersion to $newVersion")
+        Timber.d("Migrating Realm Identity from $oldVersion to $newVersion")
 
-        if (oldVersion <= 0) migrateTo1(realm)
-    }
-
-    private fun migrateTo1(realm: DynamicRealm) {
-        Timber.d("Step 0 -> 1")
-        Timber.d("Add field userConsent (Boolean) and set the value to false")
-
-        realm.schema.get("IdentityDataEntity")
-                ?.addField(IdentityDataEntityFields.USER_CONSENT, Boolean::class.java)
+        if (oldVersion < 1) MigrateIdentityTo001(realm).perform()
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/migration/MigrateIdentityTo001.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/migration/MigrateIdentityTo001.kt
new file mode 100644
index 0000000000000000000000000000000000000000..002601470d8dd9cd5839f03ceafd55ba5aa2bf93
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/migration/MigrateIdentityTo001.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.session.identity.db.migration
+
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.internal.session.identity.db.IdentityDataEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+import timber.log.Timber
+
+class MigrateIdentityTo001(realm: DynamicRealm) : RealmMigrator(realm, 1) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        Timber.d("Add field userConsent (Boolean) and set the value to false")
+        realm.schema.get("IdentityDataEntity")
+                ?.addField(IdentityDataEntityFields.USER_CONSENT, Boolean::class.java)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetPreviewUrlTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetPreviewUrlTask.kt
index e707c2351c1edc7d055d1dd5eb88c428e0cc246d..32bcf3f7ca8e6c36d682bcea3067c7d56649d73b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetPreviewUrlTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetPreviewUrlTask.kt
@@ -48,8 +48,8 @@ internal class DefaultGetPreviewUrlTask @Inject constructor(
 
     override suspend fun execute(params: GetPreviewUrlTask.Params): PreviewUrlData {
         return when (params.cacheStrategy) {
-            CacheStrategy.NoCache -> doRequest(params.url, params.timestamp)
-            is CacheStrategy.TtlCache -> doRequestWithCache(
+            CacheStrategy.NoCache       -> doRequest(params.url, params.timestamp)
+            is CacheStrategy.TtlCache   -> doRequestWithCache(
                     params.url,
                     params.timestamp,
                     params.cacheStrategy.validityDurationInMillis,
@@ -77,7 +77,9 @@ internal class DefaultGetPreviewUrlTask @Inject constructor(
                 siteName = (get("og:site_name") as? String)?.unescapeHtml(),
                 title = (get("og:title") as? String)?.unescapeHtml(),
                 description = (get("og:description") as? String)?.unescapeHtml(),
-                mxcUrl = get("og:image") as? String
+                mxcUrl = get("og:image") as? String,
+                imageHeight = (get("og:image:height") as? Double)?.toInt(),
+                imageWidth = (get("og:image:width") as? Double)?.toInt(),
         )
     }
 
@@ -114,7 +116,8 @@ internal class DefaultGetPreviewUrlTask @Inject constructor(
             previewUrlCacheEntity.title = data.title
             previewUrlCacheEntity.description = data.description
             previewUrlCacheEntity.mxcUrl = data.mxcUrl
-
+            previewUrlCacheEntity.imageHeight = data.imageHeight
+            previewUrlCacheEntity.imageWidth = data.imageWidth
             previewUrlCacheEntity.lastUpdatedTimestamp = Date().time
         }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/PreviewUrlMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/PreviewUrlMapper.kt
index dd1a9ead2642471228d5605c8c03663b4e5ae346..551dc29b926d085a1f489bf298d01d148d844b97 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/PreviewUrlMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/PreviewUrlMapper.kt
@@ -27,5 +27,7 @@ internal fun PreviewUrlCacheEntity.toDomain() = PreviewUrlData(
         siteName = siteName,
         title = title,
         description = description,
-        mxcUrl = mxcUrl
+        mxcUrl = mxcUrl,
+        imageWidth = imageWidth,
+        imageHeight = imageHeight
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt
index 3e821b8956f100211b5552a0b26c8ecfde3e3958..cdc7350f8bcde907aef51c8d5640e67502a36a66 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt
@@ -19,11 +19,13 @@ import androidx.lifecycle.LiveData
 import androidx.lifecycle.Transformations
 import com.zhuinden.monarchy.Monarchy
 import org.matrix.android.sdk.api.pushrules.Action
+import org.matrix.android.sdk.api.pushrules.ConditionResolver
 import org.matrix.android.sdk.api.pushrules.PushEvents
 import org.matrix.android.sdk.api.pushrules.PushRuleService
 import org.matrix.android.sdk.api.pushrules.RuleKind
 import org.matrix.android.sdk.api.pushrules.RuleScope
 import org.matrix.android.sdk.api.pushrules.RuleSetKey
+import org.matrix.android.sdk.api.pushrules.SenderNotificationPermissionCondition
 import org.matrix.android.sdk.api.pushrules.getActions
 import org.matrix.android.sdk.api.pushrules.rest.PushRule
 import org.matrix.android.sdk.api.pushrules.rest.RuleSet
@@ -53,6 +55,7 @@ internal class DefaultPushRuleService @Inject constructor(
         private val removePushRuleTask: RemovePushRuleTask,
         private val pushRuleFinder: PushRuleFinder,
         private val taskExecutor: TaskExecutor,
+        private val conditionResolver: ConditionResolver,
         @SessionDatabase private val monarchy: Monarchy
 ) : PushRuleService {
 
@@ -143,6 +146,10 @@ internal class DefaultPushRuleService @Inject constructor(
         return pushRuleFinder.fulfilledBingRule(event, rules)?.getActions().orEmpty()
     }
 
+    override fun resolveSenderNotificationPermissionCondition(event: Event, condition: SenderNotificationPermissionCondition): Boolean {
+        return conditionResolver.resolveSenderNotificationPermissionCondition(event, condition)
+    }
+
     override fun getKeywords(): LiveData<Set<String>> {
         // Keywords are all content rules that don't start with '.'
         val liveData = monarchy.findAllMappedWithChanges(
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 1c3d1971c2cc076acfd13df16e3311e62e7be21c..2d8c3e9c78eaecd443d207259b3d0e9998127a9c 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
@@ -35,6 +35,7 @@ import org.matrix.android.sdk.api.session.room.send.DraftService
 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.timeline.TimelineService
 import org.matrix.android.sdk.api.session.room.typing.TypingService
 import org.matrix.android.sdk.api.session.room.uploads.UploadsService
@@ -54,6 +55,7 @@ import java.security.InvalidParameterException
 internal class DefaultRoom(override val roomId: String,
                            private val roomSummaryDataSource: RoomSummaryDataSource,
                            private val timelineService: TimelineService,
+                           private val threadsService: ThreadsService,
                            private val sendService: SendService,
                            private val draftService: DraftService,
                            private val stateService: StateService,
@@ -77,6 +79,7 @@ internal class DefaultRoom(override val roomId: String,
 ) :
         Room,
         TimelineService by timelineService,
+        ThreadsService by threadsService,
         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/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt
index 7ca64aa66a7efcbc6b77b33643bd4ee294328a00..4a02c55db0fe6c007c609f8deaa89324b33ca65c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt
@@ -46,6 +46,7 @@ import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask
 import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource
 import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
 import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask
+import org.matrix.android.sdk.internal.session.room.membership.leaving.LeaveRoomTask
 import org.matrix.android.sdk.internal.session.room.peeking.PeekRoomTask
 import org.matrix.android.sdk.internal.session.room.peeking.ResolveRoomStateTask
 import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask
@@ -66,7 +67,8 @@ internal class DefaultRoomService @Inject constructor(
         private val peekRoomTask: PeekRoomTask,
         private val roomGetter: RoomGetter,
         private val roomSummaryDataSource: RoomSummaryDataSource,
-        private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource
+        private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
+        private val leaveRoomTask: LeaveRoomTask,
 ) : RoomService {
 
     override suspend fun createRoom(createRoomParams: CreateRoomParams): String {
@@ -133,6 +135,10 @@ internal class DefaultRoomService @Inject constructor(
         joinRoomTask.execute(JoinRoomTask.Params(roomId, reason, thirdPartySigned = thirdPartySigned))
     }
 
+    override suspend fun leaveRoom(roomId: String, reason: String?) {
+        leaveRoomTask.execute(LeaveRoomTask.Params(roomId, reason))
+    }
+
     override suspend fun markAllAsRead(roomIds: List<String>) {
         markAllRoomsReadTask.execute(MarkAllRoomsReadTask.Params(roomIds))
     }
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 3cc08df0e8fd7f7c2ebcd589fc5da0d197620633..acceaf6e24c34bd78d0d9ff3736e371a6a2573ed 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
@@ -44,6 +44,7 @@ import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
 import org.matrix.android.sdk.internal.SessionManager
 import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
 import org.matrix.android.sdk.internal.crypto.verification.toState
+import org.matrix.android.sdk.internal.database.helper.findRootThreadEvent
 import org.matrix.android.sdk.internal.database.mapper.ContentMapper
 import org.matrix.android.sdk.internal.database.mapper.EventMapper
 import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntity
@@ -332,6 +333,29 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                 )
             }
         }
+
+        if (!isLocalEcho) {
+            val replaceEvent = TimelineEventEntity.where(realm, roomId, eventId).findFirst()
+            handleThreadSummaryEdition(editedEvent, replaceEvent, existingSummary?.editions)
+        }
+    }
+
+    /**
+     * Check if the edition is on the latest thread event, and update it accordingly
+     */
+    private fun handleThreadSummaryEdition(editedEvent: EventEntity?,
+                                           replaceEvent: TimelineEventEntity?,
+                                           editions: List<EditionOfEvent>?) {
+        replaceEvent ?: return
+        editedEvent ?: return
+        editedEvent.findRootThreadEvent()?.apply {
+            val threadSummaryEventId = threadSummaryLatestMessage?.eventId
+            if (editedEvent.eventId == threadSummaryEventId || editions?.any { it.eventId == threadSummaryEventId } == true) {
+                // The edition is for the latest event or for any event replaced, this is to handle multiple
+                // edits of the same latest event
+                threadSummaryLatestMessage = replaceEvent
+            }
+        }
     }
 
     private fun handleResponse(realm: Realm,
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 efc5166a0cff6a217794fe9b5a9f74a1b137818d..399bfbd0e45b00e64fb790b2186241b4768f160f 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
@@ -226,7 +226,8 @@ internal interface RoomAPI {
     suspend fun getRelations(@Path("roomId") roomId: String,
                              @Path("eventId") eventId: String,
                              @Path("relationType") relationType: String,
-                             @Path("eventType") eventType: String
+                             @Path("eventType") eventType: String,
+                             @Query("limit") limit: Int? = null
     ): RelationsResponse
 
     /**
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 4ab06338a2555b06080a4cd4b2fc6858e34c8ee0..70c1ab4f42453412aa2a5b258b3979121bdcd966 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
@@ -35,6 +35,7 @@ import org.matrix.android.sdk.internal.session.room.state.DefaultStateService
 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.timeline.DefaultTimelineService
 import org.matrix.android.sdk.internal.session.room.typing.DefaultTypingService
 import org.matrix.android.sdk.internal.session.room.uploads.DefaultUploadsService
@@ -50,6 +51,7 @@ internal interface RoomFactory {
 internal class DefaultRoomFactory @Inject constructor(private val cryptoService: CryptoService,
                                                       private val roomSummaryDataSource: RoomSummaryDataSource,
                                                       private val timelineServiceFactory: DefaultTimelineService.Factory,
+                                                      private val threadsServiceFactory: DefaultThreadsService.Factory,
                                                       private val sendServiceFactory: DefaultSendService.Factory,
                                                       private val draftServiceFactory: DefaultDraftService.Factory,
                                                       private val stateServiceFactory: DefaultStateService.Factory,
@@ -76,6 +78,7 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService:
                 roomId = roomId,
                 roomSummaryDataSource = roomSummaryDataSource,
                 timelineService = timelineServiceFactory.create(roomId),
+                threadsService = threadsServiceFactory.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 64f6bc0b30db4f82c8c694fb4dfe09944d94056f..f831a77a5d7c72539c8f4339309b62206caa1d41 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,6 +77,8 @@ 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.DefaultFetchThreadTimelineTask
+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
 import org.matrix.android.sdk.internal.session.room.state.DefaultSendStateTask
@@ -289,4 +291,7 @@ internal abstract class RoomModule {
 
     @Binds
     abstract fun bindGetRoomSummaryTask(task: DefaultGetRoomSummaryTask): GetRoomSummaryTask
+
+    @Binds
+    abstract fun bindFetchThreadTimelineTask(task: DefaultFetchThreadTimelineTask): FetchThreadTimelineTask
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/accountdata/RoomAccountDataDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/accountdata/RoomAccountDataDataSource.kt
index d96beed3f1891bfcad6e02b8fde386419697b56e..d5a110dfc28993c21e16bb6deb95e5523d6254fb 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/accountdata/RoomAccountDataDataSource.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/accountdata/RoomAccountDataDataSource.kt
@@ -54,8 +54,7 @@ internal class RoomAccountDataDataSource @Inject constructor(@SessionDatabase pr
      */
     fun getAccountDataEvents(roomId: String?, types: Set<String>): List<RoomAccountDataEvent> {
         return realmSessionProvider.withRealm { realm ->
-            val roomEntity = buildRoomQuery(realm, roomId, types).findFirst() ?: return@withRealm emptyList()
-            roomEntity.accountDataEvents(types)
+            buildRoomQuery(realm, roomId, types).findAll().flatMap { it.accountDataEvents(types) }
         }
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt
index ac6e0562b0aaf19f39682293ea6815e49d495f58..9bd15a02671487dadf0ce21d14d6f9d299a86812 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt
@@ -17,7 +17,6 @@
 package org.matrix.android.sdk.internal.session.room.create
 
 import com.zhuinden.monarchy.Monarchy
-import io.realm.Realm
 import io.realm.RealmConfiguration
 import kotlinx.coroutines.TimeoutCancellationException
 import org.matrix.android.sdk.api.failure.Failure
@@ -28,6 +27,7 @@ import org.matrix.android.sdk.api.session.room.model.Membership
 import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
 import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset
 import org.matrix.android.sdk.internal.database.awaitNotEmptyResult
+import org.matrix.android.sdk.internal.database.awaitTransaction
 import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
 import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
 import org.matrix.android.sdk.internal.database.query.where
@@ -105,7 +105,7 @@ internal class DefaultCreateRoomTask @Inject constructor(
             throw CreateRoomFailure.CreatedWithTimeout(roomId)
         }
 
-        Realm.getInstance(realmConfiguration).executeTransactionAsync {
+        awaitTransaction(realmConfiguration) {
             RoomSummaryEntity.where(it, roomId).findFirst()?.lastActivityTime = System.currentTimeMillis()
         }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt
index 49b58aa7655fd7989b59942d6707dfe651f614f9..005d7f26db4f9fe86aeebcc56fe834862e6f0656 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt
@@ -37,8 +37,6 @@ import org.matrix.android.sdk.internal.query.QueryStringValueProcessor
 import org.matrix.android.sdk.internal.query.process
 import org.matrix.android.sdk.internal.session.room.membership.admin.MembershipAdminTask
 import org.matrix.android.sdk.internal.session.room.membership.joining.InviteTask
-import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask
-import org.matrix.android.sdk.internal.session.room.membership.leaving.LeaveRoomTask
 import org.matrix.android.sdk.internal.session.room.membership.threepid.InviteThreePidTask
 import org.matrix.android.sdk.internal.util.fetchCopied
 
@@ -48,8 +46,6 @@ internal class DefaultMembershipService @AssistedInject constructor(
         private val loadRoomMembersTask: LoadRoomMembersTask,
         private val inviteTask: InviteTask,
         private val inviteThreePidTask: InviteThreePidTask,
-        private val joinTask: JoinRoomTask,
-        private val leaveRoomTask: LeaveRoomTask,
         private val membershipAdminTask: MembershipAdminTask,
         @UserId
         private val userId: String,
@@ -139,14 +135,4 @@ internal class DefaultMembershipService @AssistedInject constructor(
         val params = InviteThreePidTask.Params(roomId, threePid)
         return inviteThreePidTask.execute(params)
     }
-
-    override suspend fun join(reason: String?, viaServers: List<String>) {
-        val params = JoinRoomTask.Params(roomId, reason, viaServers)
-        joinTask.execute(params)
-    }
-
-    override suspend fun leave(reason: String?) {
-        val params = LeaveRoomTask.Params(roomId, reason)
-        leaveRoomTask.execute(params)
-    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt
index 82fea237dbd75d7aa216c6c2d2090028d30203ff..22a46b6cfc96e1cad099678c5de643d81d4c5166 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt
@@ -16,7 +16,6 @@
 
 package org.matrix.android.sdk.internal.session.room.membership.joining
 
-import io.realm.Realm
 import io.realm.RealmConfiguration
 import kotlinx.coroutines.TimeoutCancellationException
 import org.matrix.android.sdk.api.session.events.model.toContent
@@ -24,6 +23,7 @@ import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure
 import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
 import org.matrix.android.sdk.api.session.room.model.Membership
 import org.matrix.android.sdk.internal.database.awaitNotEmptyResult
+import org.matrix.android.sdk.internal.database.awaitTransaction
 import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
 import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
 import org.matrix.android.sdk.internal.database.query.where
@@ -89,11 +89,9 @@ internal class DefaultJoinRoomTask @Inject constructor(
         } catch (exception: TimeoutCancellationException) {
             throw JoinRoomFailure.JoinedWithTimeout
         }
-
-        Realm.getInstance(realmConfiguration).executeTransactionAsync {
+        awaitTransaction(realmConfiguration) {
             RoomSummaryEntity.where(it, roomId).findFirst()?.lastActivityTime = System.currentTimeMillis()
         }
-
         setReadMarkers(roomId)
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt
index 5ae4007c639863d9eed4e4528a1acdda213dc8ac..ee52fe574b932ba0cf0aaac736746a51f32b5a0a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt
@@ -83,7 +83,9 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr
 //                    }
 
                     val modified = unsignedData.copy(redactedEvent = redactionEvent)
-                    eventToPrune.content = ContentMapper.map(emptyMap())
+                    // I Commented the line below, it should not be empty while we lose all the previous info about
+                    // the redacted event
+//                    eventToPrune.content = ContentMapper.map(emptyMap())
                     eventToPrune.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified)
                     eventToPrune.decryptionResultJson = null
                     eventToPrune.decryptionErrorCode = 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 cbcc108dddf3aa1905a21115a38a5b10d69ef7dc..848e14ff57bee63375f404f5c4202bd89f52601c 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
@@ -21,7 +21,6 @@ import com.zhuinden.monarchy.Monarchy
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
-import org.matrix.android.sdk.api.MatrixCallback
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
 import org.matrix.android.sdk.api.session.room.model.message.PollType
@@ -31,17 +30,15 @@ import org.matrix.android.sdk.api.util.Cancelable
 import org.matrix.android.sdk.api.util.NoOpCancellable
 import org.matrix.android.sdk.api.util.Optional
 import org.matrix.android.sdk.api.util.toOptional
-import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
 import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
 import org.matrix.android.sdk.internal.database.mapper.asDomain
 import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
 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.task.TaskExecutor
-import org.matrix.android.sdk.internal.task.configureWith
 import org.matrix.android.sdk.internal.util.fetchCopyMap
 import timber.log.Timber
 
@@ -50,13 +47,12 @@ internal class DefaultRelationService @AssistedInject constructor(
         private val eventEditor: EventEditor,
         private val eventSenderProcessor: EventSenderProcessor,
         private val eventFactory: LocalEchoEventFactory,
-        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,
-        private val taskExecutor: TaskExecutor) :
-    RelationService {
+        @SessionDatabase private val monarchy: Monarchy
+) : RelationService {
 
     @AssistedFactory
     interface Factory {
@@ -78,39 +74,31 @@ internal class DefaultRelationService @AssistedInject constructor(
                         .none { it.addedByMe && it.key == reaction }) {
             val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction)
                     .also { saveLocalEcho(it) }
-            return eventSenderProcessor.postEvent(event, false /* reaction are not encrypted*/)
+            eventSenderProcessor.postEvent(event, false /* reaction are not encrypted*/)
         } else {
             Timber.w("Reaction already added")
             NoOpCancellable
         }
     }
 
-    override fun undoReaction(targetEventId: String, reaction: String): Cancelable {
+    override suspend fun undoReaction(targetEventId: String, reaction: String): Cancelable {
         val params = FindReactionEventForUndoTask.Params(
                 roomId,
                 targetEventId,
                 reaction
         )
-        // TODO We should avoid using MatrixCallback internally
-        val callback = object : MatrixCallback<FindReactionEventForUndoTask.Result> {
-            override fun onSuccess(data: FindReactionEventForUndoTask.Result) {
-                if (data.redactEventId == null) {
-                    Timber.w("Cannot find reaction to undo (not yet synced?)")
-                    // TODO?
-                }
-                data.redactEventId?.let { toRedact ->
-                    val redactEvent = eventFactory.createRedactEvent(roomId, toRedact, null)
-                            .also { saveLocalEcho(it) }
-                    eventSenderProcessor.postRedaction(redactEvent, null)
-                }
-            }
+
+        val data = findReactionEventForUndoTask.executeRetry(params, Int.MAX_VALUE)
+
+        return if (data.redactEventId == null) {
+            Timber.w("Cannot find reaction to undo (not yet synced?)")
+            // TODO?
+            NoOpCancellable
+        } else {
+            val redactEvent = eventFactory.createRedactEvent(roomId, data.redactEventId, null)
+                    .also { saveLocalEcho(it) }
+            eventSenderProcessor.postRedaction(redactEvent, null)
         }
-        return findReactionEventForUndoTask
-                .configureWith(params) {
-                    this.retryCount = Int.MAX_VALUE
-                    this.callback = callback
-                }
-                .executeBy(taskExecutor)
     }
 
     override fun editPoll(targetEvent: TimelineEvent,
@@ -139,12 +127,24 @@ internal class DefaultRelationService @AssistedInject constructor(
         return fetchEditHistoryTask.execute(FetchEditHistoryTask.Params(roomId, eventId))
     }
 
-    override fun replyToMessage(eventReplied: TimelineEvent, replyText: CharSequence, autoMarkdown: Boolean): Cancelable? {
-        val event = eventFactory.createReplyTextEvent(roomId, eventReplied, replyText, autoMarkdown)
+    override fun replyToMessage(
+            eventReplied: TimelineEvent,
+            replyText: CharSequence,
+            autoMarkdown: Boolean,
+            showInThread: Boolean,
+            rootThreadEventId: String?
+    ): Cancelable? {
+        val event = eventFactory.createReplyTextEvent(
+                roomId = roomId,
+                eventReplied = eventReplied,
+                replyText = replyText,
+                autoMarkdown = autoMarkdown,
+                rootThreadEventId = rootThreadEventId,
+                showInThread = showInThread)
                 ?.also { saveLocalEcho(it) }
                 ?: return null
 
-        return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
+        return eventSenderProcessor.postEvent(event)
     }
 
     override fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary? {
@@ -166,6 +166,47 @@ internal class DefaultRelationService @AssistedInject constructor(
         }
     }
 
+    override fun replyInThread(
+            rootThreadEventId: String,
+            replyInThreadText: CharSequence,
+            msgType: String,
+            autoMarkdown: Boolean,
+            formattedText: String?,
+            eventReplied: TimelineEvent?): Cancelable? {
+        val event = if (eventReplied != null) {
+            // Reply within a thread
+            eventFactory.createReplyTextEvent(
+                    roomId = roomId,
+                    eventReplied = eventReplied,
+                    replyText = replyInThreadText,
+                    autoMarkdown = autoMarkdown,
+                    rootThreadEventId = rootThreadEventId,
+                    showInThread = false
+            )
+                    ?.also {
+                        saveLocalEcho(it)
+                    }
+                    ?: return null
+        } else {
+            // Normal thread reply
+            eventFactory.createThreadTextEvent(
+                    rootThreadEventId = rootThreadEventId,
+                    roomId = roomId,
+                    text = replyInThreadText,
+                    msgType = msgType,
+                    autoMarkdown = autoMarkdown,
+                    formattedText = formattedText)
+                    .also {
+                        saveLocalEcho(it)
+                    }
+        }
+        return eventSenderProcessor.postEvent(event)
+    }
+
+    override suspend fun fetchThreadTimeline(rootThreadEventId: String): Boolean {
+        return fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params(roomId, rootThreadEventId))
+    }
+
     /**
      * 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/EventEditor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt
index a40a8df4435dc72ba512e60924a80e0f246e4e31..b54cd71e5082a612ec115b1dd43057e6c45ee834 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
@@ -23,7 +23,6 @@ import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
 import org.matrix.android.sdk.api.util.Cancelable
 import org.matrix.android.sdk.api.util.NoOpCancellable
-import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
 import org.matrix.android.sdk.internal.database.mapper.toEntity
 import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
 import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
@@ -33,7 +32,6 @@ import javax.inject.Inject
 
 internal class EventEditor @Inject constructor(private val eventSenderProcessor: EventSenderProcessor,
                                                private val eventFactory: LocalEchoEventFactory,
-                                               private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
                                                private val localEchoRepository: LocalEchoRepository) {
 
     fun editTextMessage(targetEvent: TimelineEvent,
@@ -51,7 +49,7 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor:
         } else if (targetEvent.root.sendState.isSent()) {
             val event = eventFactory
                     .createReplaceTextEvent(roomId, targetEvent.eventId, newBodyText, newBodyAutoMarkdown, msgType, compatibilityBodyText)
-            return sendReplaceEvent(roomId, event)
+            return sendReplaceEvent(event)
         } else {
             // Should we throw?
             Timber.w("Can't edit a sending event")
@@ -72,7 +70,7 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor:
         } else if (targetEvent.root.sendState.isSent()) {
             val event = eventFactory
                     .createPollReplaceEvent(roomId, pollType, targetEvent.eventId, question, options)
-            return sendReplaceEvent(roomId, event)
+            return sendReplaceEvent(event)
         } else {
             Timber.w("Can't edit a sending event")
             return NoOpCancellable
@@ -82,12 +80,12 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor:
     private fun sendFailedEvent(targetEvent: TimelineEvent, editedEvent: Event): Cancelable {
         val roomId = targetEvent.roomId
         updateFailedEchoWithEvent(roomId, targetEvent.eventId, editedEvent)
-        return eventSenderProcessor.postEvent(editedEvent, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
+        return eventSenderProcessor.postEvent(editedEvent)
     }
 
-    private fun sendReplaceEvent(roomId: String, editedEvent: Event): Cancelable {
+    private fun sendReplaceEvent(editedEvent: Event): Cancelable {
         localEchoRepository.createLocalEcho(editedEvent)
-        return eventSenderProcessor.postEvent(editedEvent, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
+        return eventSenderProcessor.postEvent(editedEvent)
     }
 
     fun editReply(replyToEdit: TimelineEvent,
@@ -97,11 +95,17 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor:
         val roomId = replyToEdit.roomId
         if (replyToEdit.root.sendState.hasFailed()) {
             // We create a new in memory event for the EventSenderProcessor but we keep the eventId of the failed event.
-            val editedEvent = eventFactory.createReplyTextEvent(roomId, originalTimelineEvent, newBodyText, false)?.copy(
+            val editedEvent = eventFactory.createReplyTextEvent(
+                    roomId = roomId,
+                    eventReplied = originalTimelineEvent,
+                    replyText = newBodyText,
+                    autoMarkdown = false,
+                    showInThread = false
+            )?.copy(
                     eventId = replyToEdit.eventId
             ) ?: return NoOpCancellable
             updateFailedEchoWithEvent(roomId, replyToEdit.eventId, editedEvent)
-            return eventSenderProcessor.postEvent(editedEvent, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
+            return eventSenderProcessor.postEvent(editedEvent)
         } else if (replyToEdit.root.sendState.isSent()) {
             val event = eventFactory.createReplaceTextOfReply(
                     roomId,
@@ -113,7 +117,7 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor:
                     compatibilityBodyText
             )
                     .also { localEchoRepository.createLocalEcho(it) }
-            return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
+            return eventSenderProcessor.postEvent(event)
         } else {
             // Should we throw?
             Timber.w("Can't edit a sending event")
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
new file mode 100644
index 0000000000000000000000000000000000000000..e0d501c515f7ea3ec595227d813c9d62a0f5fa6a
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.matrix.android.sdk.internal.session.room.relation.threads
+
+import com.zhuinden.monarchy.Monarchy
+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
+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
+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.ReactionAggregatedSummaryEntity
+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.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.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.events.getFixedRoomMemberContent
+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.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> {
+    data class Params(
+            val roomId: String,
+            val rootThreadEventId: String
+    )
+}
+
+internal class DefaultFetchThreadTimelineTask @Inject constructor(
+        private val roomAPI: RoomAPI,
+        private val globalErrorReceiver: GlobalErrorReceiver,
+        private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
+        @SessionDatabase private val monarchy: Monarchy,
+        @UserId private val userId: String,
+        private val cryptoService: DefaultCryptoService
+) : FetchThreadTimelineTask {
+
+    override suspend fun execute(params: FetchThreadTimelineTask.Params): Boolean {
+        val isRoomEncrypted = cryptoSessionInfoProvider.isRoomEncrypted(params.roomId)
+        val response = executeRequest(globalErrorReceiver) {
+            roomAPI.getRelations(
+                    roomId = params.roomId,
+                    eventId = params.rootThreadEventId,
+                    relationType = RelationType.IO_THREAD,
+                    eventType = if (isRoomEncrypted) EventType.ENCRYPTED else EventType.MESSAGE,
+                    limit = 2000
+            )
+        }
+
+        val threadList = response.chunks + listOfNotNull(response.originalEvent)
+
+        return storeNewEventsIfNeeded(threadList, params.roomId)
+    }
+
+    /**
+     * Store new events if they are not already received, and returns weather or not,
+     * a timeline update should be made
+     * @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
+                        }
+                    }
+
+                    optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(
+                            roomId = roomId,
+                            realm = realm,
+                            currentUserId = userId,
+                            shouldUpdateNotifications = false
+                    )
+                }
+        Timber.i("----> size: ${threadList.size} | skipped: $eventsSkipped | threads: ${threadList.map { it.eventId }}")
+
+        return eventsSkipped == threadList.size
+    }
+
+    /**
+     * 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
+            val result = cryptoService.decryptEvent(event.copy(roomId = roomId), "")
+            event.mxDecryptionResult = OlmDecryptionResult(
+                    payload = result.clearEvent,
+                    senderKey = result.senderCurve25519Key,
+                    keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
+                    forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
+            )
+        } catch (e: MXCryptoError) {
+            if (e is MXCryptoError.Base) {
+                event.mCryptoError = e.errorType
+                event.mCryptoErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription
+            }
+        }
+    }
+
+    private fun handleReaction(realm: Realm,
+                               event: Event,
+                               roomId: String) {
+        val unsignedData = event.unsignedData ?: return
+        val relatedEventId = event.eventId ?: return
+
+        unsignedData.relations?.annotations?.chunk?.forEach { relationChunk ->
+
+            if (relationChunk.type == EventType.REACTION) {
+                val reaction = relationChunk.key
+                Timber.i("----> Annotation found in ${event.eventId} ${relationChunk.key} ")
+
+                val eventSummary = EventAnnotationsSummaryEntity.getOrCreate(realm, roomId, relatedEventId)
+                var sum = eventSummary.reactionsSummary.find { it.key == reaction }
+
+                if (sum == null) {
+                    sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java)
+                    sum.key = reaction
+                    sum.firstTimestamp = event.originServerTs ?: 0
+                    Timber.v("Adding synced reaction $reaction")
+                    sum.count = 1
+                    // reactionEventId not included in the /relations API
+//                    sum.sourceEvents.add(reactionEventId)
+                    eventSummary.reactionsSummary.add(sum)
+                } else {
+                    sum.count += 1
+                }
+            }
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
index 5662a72cb82e5336a11e2604f32cad19ccf29e11..28c17f38b6a51cf881d1d19c6aea439f246d5fd6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
@@ -46,7 +46,7 @@ import org.matrix.android.sdk.api.util.Cancelable
 import org.matrix.android.sdk.api.util.CancelableBag
 import org.matrix.android.sdk.api.util.JsonDict
 import org.matrix.android.sdk.api.util.NoOpCancellable
-import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
+import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 import org.matrix.android.sdk.internal.di.SessionId
 import org.matrix.android.sdk.internal.di.WorkManagerProvider
 import org.matrix.android.sdk.internal.session.content.UploadContentWorker
@@ -66,7 +66,7 @@ internal class DefaultSendService @AssistedInject constructor(
         private val workManagerProvider: WorkManagerProvider,
         @SessionId private val sessionId: String,
         private val localEchoEventFactory: LocalEchoEventFactory,
-        private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
+        private val cryptoStore: IMXCryptoStore,
         private val taskExecutor: TaskExecutor,
         private val localEchoRepository: LocalEchoRepository,
         private val eventSenderProcessor: EventSenderProcessor,
@@ -98,8 +98,14 @@ internal class DefaultSendService @AssistedInject constructor(
                 .let { sendEvent(it) }
     }
 
-    override fun sendQuotedTextMessage(quotedEvent: TimelineEvent, text: String, autoMarkdown: Boolean): Cancelable {
-        return localEchoEventFactory.createQuotedTextEvent(roomId, quotedEvent, text, autoMarkdown)
+    override fun sendQuotedTextMessage(quotedEvent: TimelineEvent, text: String, autoMarkdown: Boolean, rootThreadEventId: String?): Cancelable {
+        return localEchoEventFactory.createQuotedTextEvent(
+                roomId = roomId,
+                quotedEvent = quotedEvent,
+                text = text,
+                autoMarkdown = autoMarkdown,
+                rootThreadEventId = rootThreadEventId
+        )
                 .also { createLocalEcho(it) }
                 .let { sendEvent(it) }
     }
@@ -254,22 +260,37 @@ internal class DefaultSendService @AssistedInject constructor(
 
     override fun sendMedias(attachments: List<ContentAttachmentData>,
                             compressBeforeSending: Boolean,
-                            roomIds: Set<String>): Cancelable {
+                            roomIds: Set<String>,
+                            rootThreadEventId: String?
+    ): Cancelable {
         return attachments.mapTo(CancelableBag()) {
-            sendMedia(it, compressBeforeSending, roomIds)
+            sendMedia(
+                    attachment = it,
+                    compressBeforeSending = compressBeforeSending,
+                    roomIds = roomIds,
+                    rootThreadEventId = rootThreadEventId)
         }
     }
 
     override fun sendMedia(attachment: ContentAttachmentData,
                            compressBeforeSending: Boolean,
-                           roomIds: Set<String>): Cancelable {
+                           roomIds: Set<String>,
+                           rootThreadEventId: String?
+    ): Cancelable {
+        // Ensure that the event will not be send in a thread if we are a different flow.
+        // Like sending files to multiple rooms
+        val rootThreadId = if (roomIds.isNotEmpty()) null else rootThreadEventId
+
         // Create an event with the media file path
         // Ensure current roomId is included in the set
         val allRoomIds = (roomIds + roomId).toList()
 
         // Create local echo for each room
         val allLocalEchoes = allRoomIds.map {
-            localEchoEventFactory.createMediaEvent(it, attachment).also { event ->
+            localEchoEventFactory.createMediaEvent(
+                    roomId = it,
+                    attachment = attachment,
+                    rootThreadEventId = rootThreadId).also { event ->
                 createLocalEcho(event)
             }
         }
@@ -282,7 +303,7 @@ internal class DefaultSendService @AssistedInject constructor(
     private fun internalSendMedia(allLocalEchoes: List<Event>, attachment: ContentAttachmentData, compressBeforeSending: Boolean): Cancelable {
         val cancelableBag = CancelableBag()
 
-        allLocalEchoes.groupBy { cryptoSessionInfoProvider.isRoomEncrypted(it.roomId!!) }
+        allLocalEchoes.groupBy { cryptoStore.roomWasOnceEncrypted(it.roomId!!) }
                 .apply {
                     keys.forEach { isRoomEncrypted ->
                         // Should never be empty
@@ -313,7 +334,7 @@ internal class DefaultSendService @AssistedInject constructor(
     }
 
     private fun sendEvent(event: Event): Cancelable {
-        return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(event.roomId!!))
+        return eventSenderProcessor.postEvent(event)
     }
 
     private fun createLocalEcho(event: Event) {
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 1e46602411cd68a8cd97c3bdaa294e1b7a8ecec3..3c36d587107c55df72f09177a2cbafc6233359fd 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
@@ -28,6 +28,7 @@ import org.matrix.android.sdk.api.session.events.model.LocalEcho
 import org.matrix.android.sdk.api.session.events.model.RelationType
 import org.matrix.android.sdk.api.session.events.model.UnsignedData
 import org.matrix.android.sdk.api.session.events.model.toContent
+import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.session.room.model.message.AudioInfo
 import org.matrix.android.sdk.api.session.room.model.message.AudioWaveformInfo
 import org.matrix.android.sdk.api.session.room.model.message.FileInfo
@@ -45,6 +46,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageLocationContent
 import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
 import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent
+import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageType
 import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
@@ -292,13 +294,16 @@ internal class LocalEchoEventFactory @Inject constructor(
                 ))
     }
 
-    fun createMediaEvent(roomId: String, attachment: ContentAttachmentData): Event {
+    fun createMediaEvent(roomId: String,
+                         attachment: ContentAttachmentData,
+                         rootThreadEventId: String?
+    ): Event {
         return when (attachment.type) {
-            ContentAttachmentData.Type.IMAGE         -> createImageEvent(roomId, attachment)
-            ContentAttachmentData.Type.VIDEO         -> createVideoEvent(roomId, attachment)
-            ContentAttachmentData.Type.AUDIO         -> createAudioEvent(roomId, attachment, isVoiceMessage = false)
-            ContentAttachmentData.Type.VOICE_MESSAGE -> createAudioEvent(roomId, attachment, isVoiceMessage = true)
-            ContentAttachmentData.Type.FILE          -> createFileEvent(roomId, attachment)
+            ContentAttachmentData.Type.IMAGE         -> createImageEvent(roomId, attachment, rootThreadEventId)
+            ContentAttachmentData.Type.VIDEO         -> createVideoEvent(roomId, attachment, rootThreadEventId)
+            ContentAttachmentData.Type.AUDIO         -> createAudioEvent(roomId, attachment, isVoiceMessage = false, rootThreadEventId = rootThreadEventId)
+            ContentAttachmentData.Type.VOICE_MESSAGE -> createAudioEvent(roomId, attachment, isVoiceMessage = true, rootThreadEventId = rootThreadEventId)
+            ContentAttachmentData.Type.FILE          -> createFileEvent(roomId, attachment, rootThreadEventId)
         }
     }
 
@@ -321,7 +326,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                 unsignedData = UnsignedData(age = null, transactionId = localId))
     }
 
-    private fun createImageEvent(roomId: String, attachment: ContentAttachmentData): Event {
+    private fun createImageEvent(roomId: String, attachment: ContentAttachmentData, rootThreadEventId: String?): Event {
         var width = attachment.width
         var height = attachment.height
 
@@ -345,12 +350,19 @@ internal class LocalEchoEventFactory @Inject constructor(
                         height = height?.toInt() ?: 0,
                         size = attachment.size
                 ),
-                url = attachment.queryUri.toString()
+                url = attachment.queryUri.toString(),
+                relatesTo = rootThreadEventId?.let {
+                    RelationDefaultContent(
+                            type = RelationType.IO_THREAD,
+                            eventId = it,
+                            inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
+                    )
+                }
         )
         return createMessageEvent(roomId, content)
     }
 
-    private fun createVideoEvent(roomId: String, attachment: ContentAttachmentData): Event {
+    private fun createVideoEvent(roomId: String, attachment: ContentAttachmentData, rootThreadEventId: String?): Event {
         val mediaDataRetriever = MediaMetadataRetriever()
         mediaDataRetriever.setDataSource(context, attachment.queryUri)
 
@@ -381,12 +393,23 @@ internal class LocalEchoEventFactory @Inject constructor(
                         thumbnailUrl = attachment.queryUri.toString(),
                         thumbnailInfo = thumbnailInfo
                 ),
-                url = attachment.queryUri.toString()
+                url = attachment.queryUri.toString(),
+                relatesTo = rootThreadEventId?.let {
+                    RelationDefaultContent(
+                            type = RelationType.IO_THREAD,
+                            eventId = it,
+                            inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
+                    )
+                }
         )
         return createMessageEvent(roomId, content)
     }
 
-    private fun createAudioEvent(roomId: String, attachment: ContentAttachmentData, isVoiceMessage: Boolean): Event {
+    private fun createAudioEvent(roomId: String,
+                                 attachment: ContentAttachmentData,
+                                 isVoiceMessage: Boolean,
+                                 rootThreadEventId: String?
+    ): Event {
         val content = MessageAudioContent(
                 msgType = MessageType.MSGTYPE_AUDIO,
                 body = attachment.name ?: "audio",
@@ -400,12 +423,19 @@ internal class LocalEchoEventFactory @Inject constructor(
                         duration = attachment.duration?.toInt(),
                         waveform = waveformSanitizer.sanitize(attachment.waveform)
                 ),
-                voiceMessageIndicator = if (!isVoiceMessage) null else emptyMap()
+                voiceMessageIndicator = if (!isVoiceMessage) null else emptyMap(),
+                relatesTo = rootThreadEventId?.let {
+                    RelationDefaultContent(
+                            type = RelationType.IO_THREAD,
+                            eventId = it,
+                            inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
+                    )
+                }
         )
         return createMessageEvent(roomId, content)
     }
 
-    private fun createFileEvent(roomId: String, attachment: ContentAttachmentData): Event {
+    private fun createFileEvent(roomId: String, attachment: ContentAttachmentData, rootThreadEventId: String?): Event {
         val content = MessageFileContent(
                 msgType = MessageType.MSGTYPE_FILE,
                 body = attachment.name ?: "file",
@@ -413,7 +443,14 @@ internal class LocalEchoEventFactory @Inject constructor(
                         mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() },
                         size = attachment.size
                 ),
-                url = attachment.queryUri.toString()
+                url = attachment.queryUri.toString(),
+                relatesTo = rootThreadEventId?.let {
+                    RelationDefaultContent(
+                            type = RelationType.IO_THREAD,
+                            eventId = it,
+                            inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
+                    )
+                }
         )
         return createMessageEvent(roomId, content)
     }
@@ -423,6 +460,7 @@ internal class LocalEchoEventFactory @Inject constructor(
     }
 
     fun createEvent(roomId: String, type: String, content: Content?): Event {
+        val newContent = enhanceStickerIfNeeded(type, content) ?: content
         val localId = LocalEcho.createLocalEchoId()
         return Event(
                 roomId = roomId,
@@ -430,19 +468,65 @@ internal class LocalEchoEventFactory @Inject constructor(
                 senderId = userId,
                 eventId = localId,
                 type = type,
-                content = content,
+                content = newContent,
                 unsignedData = UnsignedData(age = null, transactionId = localId)
         )
     }
 
+    /**
+     * Enhance sticker to support threads fallback if needed
+     */
+    private fun enhanceStickerIfNeeded(type: String, content: Content?): Content? {
+        var newContent: Content? = null
+        if (type == EventType.STICKER) {
+            val isThread = (content.toModel<MessageStickerContent>())?.relatesTo?.type == RelationType.IO_THREAD
+            val rootThreadEventId = (content.toModel<MessageStickerContent>())?.relatesTo?.eventId
+            if (isThread && rootThreadEventId != null) {
+                val newRelationalDefaultContent = (content.toModel<MessageStickerContent>())?.relatesTo?.copy(
+                        inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId))
+                )
+                newContent = (content.toModel<MessageStickerContent>())?.copy(
+                        relatesTo = newRelationalDefaultContent
+                ).toContent()
+            }
+        }
+        return newContent
+    }
+
+    /**
+     * Creates a thread event related to the already existing root event
+     */
+    fun createThreadTextEvent(
+            rootThreadEventId: String,
+            roomId: String,
+            text: CharSequence,
+            msgType: String,
+            autoMarkdown: Boolean,
+            formattedText: String?): Event {
+        val content = formattedText?.let { TextContent(text.toString(), it) } ?: createTextContent(text, autoMarkdown)
+        return createEvent(
+                roomId,
+                EventType.MESSAGE,
+                content.toThreadTextContent(
+                        rootThreadEventId = rootThreadEventId,
+                        latestThreadEventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId),
+                        msgType = msgType)
+                        .toContent())
+    }
+
     private fun dummyOriginServerTs(): Long {
         return System.currentTimeMillis()
     }
 
+    /**
+     * Creates a reply to a regular timeline Event or a thread Event if needed
+     */
     fun createReplyTextEvent(roomId: String,
                              eventReplied: TimelineEvent,
                              replyText: CharSequence,
-                             autoMarkdown: Boolean): Event? {
+                             autoMarkdown: Boolean,
+                             rootThreadEventId: String? = null,
+                             showInThread: Boolean): Event? {
         // Fallbacks and event representation
         // TODO Add error/warning logs when any of this is null
         val permalink = permalinkFactory.createPermalink(eventReplied.root, false) ?: return null
@@ -473,11 +557,33 @@ internal class LocalEchoEventFactory @Inject constructor(
                 format = MessageFormat.FORMAT_MATRIX_HTML,
                 body = replyFallback,
                 formattedBody = replyFormatted,
-                relatesTo = RelationDefaultContent(null, null, ReplyToContent(eventId))
-        )
+                relatesTo = generateReplyRelationContent(
+                        eventId = eventId,
+                        rootThreadEventId = rootThreadEventId,
+                        showAsReply = showInThread))
         return createMessageEvent(roomId, content)
     }
 
+    /**
+     * Generates the appropriate relatesTo object for a reply event.
+     * It can either be a regular reply or a reply within a thread
+     * "m.relates_to": {
+     *      "rel_type": "m.thread",
+     *      "event_id": "$thread_root",
+     *      "m.in_reply_to": {
+     *          "event_id": "$event_target",
+     *          "render_in": ["m.thread"]
+     *        }
+     *   }
+     */
+    private fun generateReplyRelationContent(eventId: String, rootThreadEventId: String? = null, showAsReply: Boolean): RelationDefaultContent =
+            rootThreadEventId?.let {
+                RelationDefaultContent(
+                        type = RelationType.IO_THREAD,
+                        eventId = it,
+                        inReplyTo = ReplyToContent(eventId = eventId, renderIn = if (showAsReply) arrayListOf("m.thread") else null))
+            } ?: RelationDefaultContent(null, null, ReplyToContent(eventId = eventId))
+
     private fun buildFormattedReply(permalink: String, userLink: String, userId: String, bodyFormatted: String, newBodyFormatted: String): String {
         return REPLY_PATTERN.format(
                 permalink,
@@ -488,6 +594,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                 newBodyFormatted
         )
     }
+
     private fun buildReplyFallback(body: TextContent, originalSenderId: String?, newBodyText: String): String {
         return buildString {
             append("> <")
@@ -593,11 +700,28 @@ internal class LocalEchoEventFactory @Inject constructor(
             quotedEvent: TimelineEvent,
             text: String,
             autoMarkdown: Boolean,
+            rootThreadEventId: String?
     ): Event {
         val messageContent = quotedEvent.getLastMessageContent()
         val textMsg = messageContent?.body
         val quoteText = legacyRiotQuoteText(textMsg, text)
-        return createFormattedTextEvent(roomId, markdownParser.parse(quoteText, force = true, advanced = autoMarkdown), MessageType.MSGTYPE_TEXT)
+
+        return if (rootThreadEventId != null) {
+            createMessageEvent(
+                    roomId,
+                    markdownParser
+                            .parse(quoteText, force = true, advanced = autoMarkdown)
+                            .toThreadTextContent(
+                                    rootThreadEventId = rootThreadEventId,
+                                    latestThreadEventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId),
+                                    msgType = MessageType.MSGTYPE_TEXT)
+            )
+        } else {
+            createFormattedTextEvent(
+                    roomId,
+                    markdownParser.parse(quoteText, force = true, advanced = autoMarkdown),
+                    MessageType.MSGTYPE_TEXT)
+        }
     }
 
     private fun legacyRiotQuoteText(quotedText: String?, myText: String): String {
@@ -631,6 +755,7 @@ internal class LocalEchoEventFactory @Inject constructor(
         // </mx-reply>
         // No whitespace because currently breaks temporary formatted text to Span
         const val REPLY_PATTERN = """<mx-reply><blockquote><a href="%s">In reply to</a> <a href="%s">%s</a><br />%s</blockquote></mx-reply>%s"""
+        const val QUOTE_PATTERN = """<blockquote><p>%s</p></blockquote><p>%s</p>"""
 
         // This is used to replace inner mx-reply tags
         val MX_REPLY_REGEX = "<mx-reply>.*</mx-reply>".toRegex()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt
index 13095fbd583f97732ead464fa5cd0f03277c4d30..1b1a66a1c418acae50f4e109b1403346e1e76cbf 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt
@@ -138,7 +138,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
         }
     }
 
-     fun deleteFailedEchoAsync(roomId: String, eventId: String?) {
+    fun deleteFailedEchoAsync(roomId: String, eventId: String?) {
         monarchy.runTransactionSync { realm ->
             TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId ?: "").findFirst()?.deleteFromRealm()
             EventEntity.where(realm, eventId = eventId ?: "").findFirst()?.deleteFromRealm()
@@ -215,4 +215,13 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
                     }
         }
     }
+
+    /**
+     * Returns the latest known thread event message, or the rootThreadEventId if no other event found
+     */
+    fun getLatestThreadEvent(rootThreadEventId: String): String {
+        return realmSessionProvider.withRealm { realm ->
+            EventEntity.where(realm, eventId = rootThreadEventId).findFirst()?.threadSummaryLatestMessage?.eventId
+        } ?: rootThreadEventId
+    }
 }
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 efc0b55abf88e278cd8063f77466fbd9b30ed0c6..5c629f87f0e31608e79b6a6ee7a98dfcf2bf354d 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
@@ -16,9 +16,12 @@
 
 package org.matrix.android.sdk.internal.session.room.send
 
+import org.matrix.android.sdk.api.session.events.model.RelationType
 import org.matrix.android.sdk.api.session.room.model.message.MessageFormat
 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.model.relation.ReplyToContent
 import org.matrix.android.sdk.api.util.ContentUtils.extractUsefulTextFromHtmlReply
 import org.matrix.android.sdk.api.util.ContentUtils.extractUsefulTextFromReply
 
@@ -41,6 +44,29 @@ fun TextContent.toMessageTextContent(msgType: String = MessageType.MSGTYPE_TEXT)
     )
 }
 
+/**
+ * Transform a TextContent to a thread message content. It will also add the inReplyTo
+ * latestThreadEventId in order for the clients without threads enabled to render it appropriately
+ * If latest event not found, we pass rootThreadEventId
+ */
+fun TextContent.toThreadTextContent(
+        rootThreadEventId: String,
+        latestThreadEventId: String,
+        msgType: String = MessageType.MSGTYPE_TEXT): MessageTextContent {
+    return MessageTextContent(
+            msgType = msgType,
+            format = MessageFormat.FORMAT_MATRIX_HTML.takeIf { formattedText != null },
+            body = text,
+            relatesTo = RelationDefaultContent(
+                    type = RelationType.IO_THREAD,
+                    eventId = rootThreadEventId,
+                    inReplyTo = ReplyToContent(
+                            eventId = latestThreadEventId
+                    )),
+            formattedBody = formattedText
+    )
+}
+
 fun TextContent.removeInReplyFallbacks(): TextContent {
     return copy(
             text = extractUsefulTextFromReply(this.text),
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt
index 33cb0db243e19914998bb42853069b015d90bae7..ccbfbfcded2ab238efe4fff13ee6f84f93b81bf5 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt
@@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.session.room.send.pills
 
 import android.text.SpannableString
 import org.matrix.android.sdk.api.session.room.send.MatrixItemSpan
+import org.matrix.android.sdk.api.util.MatrixItem
 import org.matrix.android.sdk.internal.session.displayname.DisplayNameResolver
 import java.util.Collections
 import javax.inject.Inject
@@ -51,6 +52,8 @@ internal class TextPillsUtils @Inject constructor(
         val pills = spannableString
                 ?.getSpans(0, text.length, MatrixItemSpan::class.java)
                 ?.map { MentionLinkSpec(it, spannableString.getSpanStart(it), spannableString.getSpanEnd(it)) }
+                // we use the raw text for @room notification instead of a link
+                ?.filterNot { it.span.matrixItem is MatrixItem.EveryoneInRoomItem }
                 ?.toMutableList()
                 ?.takeIf { it.isNotEmpty() }
                 ?: return null
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt
index eb691616140e3e25a1e4f50cc4699abf20e4fd00..5b4efa5df6fdf26ccfaddb7ff145043cafc26d7a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt
@@ -26,9 +26,9 @@ import org.matrix.android.sdk.api.failure.Failure
 import org.matrix.android.sdk.api.failure.getRetryDelay
 import org.matrix.android.sdk.api.failure.isLimitExceededError
 import org.matrix.android.sdk.api.session.Session
-import org.matrix.android.sdk.api.session.crypto.CryptoService
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.util.Cancelable
+import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 import org.matrix.android.sdk.internal.session.SessionScope
 import org.matrix.android.sdk.internal.task.CoroutineSequencer
 import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer
@@ -54,7 +54,7 @@ private const val MAX_RETRY_COUNT = 3
  */
 @SessionScope
 internal class EventSenderProcessorCoroutine @Inject constructor(
-        private val cryptoService: CryptoService,
+        private val cryptoStore: IMXCryptoStore,
         private val sessionParams: SessionParams,
         private val queuedTaskFactory: QueuedTaskFactory,
         private val taskExecutor: TaskExecutor,
@@ -92,7 +92,8 @@ internal class EventSenderProcessorCoroutine @Inject constructor(
     }
 
     override fun postEvent(event: Event): Cancelable {
-        return postEvent(event, event.roomId?.let { cryptoService.isRoomEncrypted(it) } ?: false)
+        val shouldEncrypt = event.roomId?.let { cryptoStore.roomWasOnceEncrypted(it) } ?: false
+        return postEvent(event, shouldEncrypt)
     }
 
     override fun postEvent(event: Event, encrypt: Boolean): Cancelable {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt
index a7887d77f87680af0ec0bc274eec2fd946390ea7..1c1d59fb3d23f119b0d5d87426adf1244b156bb1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt
@@ -119,9 +119,8 @@ internal class RoomSummaryUpdater @Inject constructor(
         roomSummaryEntity.roomType = roomType
         Timber.v("## Space: Updating summary room [$roomId] roomType: [$roomType]")
 
-        // Don't use current state for this one as we are only interested in having MXCRYPTO_ALGORITHM_MEGOLM event in the room
         val encryptionEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ENCRYPTION, stateKey = "")?.root
-        Timber.v("## CRYPTO: currentEncryptionEvent is $encryptionEvent")
+        Timber.d("## CRYPTO: currentEncryptionEvent is $encryptionEvent")
 
         val latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
 
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
new file mode 100644
index 0000000000000000000000000000000000000000..5967ae8d2edd2a11d7653fe52ea95565ccd75364
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.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
+
+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.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.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 DefaultThreadsService @AssistedInject constructor(
+        @Assisted private val roomId: String,
+        @UserId private val userId: String,
+        @SessionDatabase private val monarchy: Monarchy,
+        private val timelineEventMapper: TimelineEventMapper,
+) : ThreadsService {
+
+    @AssistedFactory
+    interface Factory {
+        fun create(roomId: String): DefaultThreadsService
+    }
+
+    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/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
index 71823cd4585ae113c996af427a27e2e29b321f65..3dd4225b2c3fdee5e12c4062ff875f61d70294bf 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
@@ -35,6 +35,7 @@ import org.matrix.android.sdk.api.extensions.tryOrNull
 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.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.sync.handler.room.ReadReceiptHandler
@@ -60,6 +61,7 @@ internal class DefaultTimeline(private val roomId: String,
                                timelineEventMapper: TimelineEventMapper,
                                timelineInput: TimelineInput,
                                threadsAwarenessHandler: ThreadsAwarenessHandler,
+                               lightweightSettingsStorage: LightweightSettingsStorage,
                                eventDecryptor: TimelineEventDecryptor) : Timeline {
 
     companion object {
@@ -79,6 +81,9 @@ internal class DefaultTimeline(private val roomId: String,
     private val sequencer = SemaphoreCoroutineSequencer()
     private val postSnapshotSignalFlow = MutableSharedFlow<Unit>(0)
 
+    private var isFromThreadTimeline = false
+    private var rootThreadEventId: String? = null
+
     private val strategyDependencies = LoadTimelineStrategy.Dependencies(
             timelineSettings = settings,
             realm = backgroundRealm,
@@ -89,6 +94,7 @@ internal class DefaultTimeline(private val roomId: String,
             timelineInput = timelineInput,
             timelineEventMapper = timelineEventMapper,
             threadsAwarenessHandler = threadsAwarenessHandler,
+            lightweightSettingsStorage = lightweightSettingsStorage,
             onEventsUpdated = this::sendSignalToPostSnapshot,
             onLimitedTimeline = this::onLimitedTimeline,
             onNewTimelineEvents = this::onNewTimelineEvents
@@ -118,18 +124,21 @@ internal class DefaultTimeline(private val roomId: String,
         listeners.clear()
     }
 
-    override fun start() {
+    override fun start(rootThreadEventId: String?) {
         timelineScope.launch {
             loadRoomMembersIfNeeded()
         }
         timelineScope.launch {
             sequencer.post {
                 if (isStarted.compareAndSet(false, true)) {
+                    isFromThreadTimeline = rootThreadEventId != null
+                    this@DefaultTimeline.rootThreadEventId = rootThreadEventId
+                    // /
                     val realm = Realm.getInstance(realmConfiguration)
                     ensureReadReceiptAreLoaded(realm)
                     backgroundRealm.set(realm)
                     listenToPostSnapshotSignals()
-                    openAround(initialEventId)
+                    openAround(initialEventId, rootThreadEventId)
                     postSnapshot()
                 }
             }
@@ -150,7 +159,7 @@ internal class DefaultTimeline(private val roomId: String,
 
     override fun restartWithEventId(eventId: String?) {
         timelineScope.launch {
-            openAround(eventId)
+            openAround(eventId, rootThreadEventId)
             postSnapshot()
         }
     }
@@ -219,19 +228,24 @@ internal class DefaultTimeline(private val roomId: String,
         return true
     }
 
-    private suspend fun openAround(eventId: String?) = withContext(timelineDispatcher) {
+    private suspend fun openAround(eventId: String?, rootThreadEventId: String?) = withContext(timelineDispatcher) {
         val baseLogMessage = "openAround(eventId: $eventId)"
         Timber.v("$baseLogMessage started")
         if (!isStarted.get()) {
             throw IllegalStateException("You should call start before using timeline")
         }
         strategy.onStop()
-        strategy = if (eventId == null) {
-            buildStrategy(LoadTimelineStrategy.Mode.Live)
-        } else {
-            buildStrategy(LoadTimelineStrategy.Mode.Permalink(eventId))
+
+        strategy = when {
+            rootThreadEventId != null -> buildStrategy(LoadTimelineStrategy.Mode.Thread(rootThreadEventId))
+            eventId == null           -> buildStrategy(LoadTimelineStrategy.Mode.Live)
+            else                      -> buildStrategy(LoadTimelineStrategy.Mode.Permalink(eventId))
         }
-        initPaginationStates(eventId)
+
+        rootThreadEventId?.let {
+            initPaginationStates(null)
+        } ?: initPaginationStates(eventId)
+
         strategy.onStart()
         loadMore(
                 count = strategyDependencies.timelineSettings.initialSize,
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 126374b430c48165916bf193b601bed7bc922bc3..d7d61f0b478594c49073b857d4d46d648f10848d 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
@@ -32,11 +32,13 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineService
 import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
 import org.matrix.android.sdk.api.util.Optional
 import org.matrix.android.sdk.internal.database.RealmSessionProvider
+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.TimelineEventEntity
 import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
 import org.matrix.android.sdk.internal.database.query.where
 import org.matrix.android.sdk.internal.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.sync.handler.room.ReadReceiptHandler
 import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
@@ -44,6 +46,7 @@ import org.matrix.android.sdk.internal.task.TaskExecutor
 
 internal class DefaultTimelineService @AssistedInject constructor(
         @Assisted private val roomId: String,
+        @UserId private val userId: String,
         @SessionDatabase private val monarchy: Monarchy,
         private val realmSessionProvider: RealmSessionProvider,
         private val timelineInput: TimelineInput,
@@ -55,6 +58,7 @@ internal class DefaultTimelineService @AssistedInject constructor(
         private val timelineEventMapper: TimelineEventMapper,
         private val loadRoomMembersTask: LoadRoomMembersTask,
         private val threadsAwarenessHandler: ThreadsAwarenessHandler,
+        private val lightweightSettingsStorage: LightweightSettingsStorage,
         private val readReceiptHandler: ReadReceiptHandler,
         private val coroutineDispatchers: MatrixCoroutineDispatchers
 ) : TimelineService {
@@ -79,7 +83,8 @@ internal class DefaultTimelineService @AssistedInject constructor(
                 loadRoomMembersTask = loadRoomMembersTask,
                 readReceiptHandler = readReceiptHandler,
                 getEventTask = contextOfEventTask,
-                threadsAwarenessHandler = threadsAwarenessHandler
+                threadsAwarenessHandler = threadsAwarenessHandler,
+                lightweightSettingsStorage = lightweightSettingsStorage
         )
     }
 
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 528b564e8b13331b30c186199644b6caa40bd480..f332c4a35f608fd367b7c8d02b200ac331899ca9 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
@@ -26,6 +26,7 @@ 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.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
@@ -51,6 +52,7 @@ internal class LoadTimelineStrategy(
     sealed interface Mode {
         object Live : Mode
         data class Permalink(val originEventId: String) : Mode
+        data class Thread(val rootThreadEventId: String) : Mode
 
         fun originEventId(): String? {
             return if (this is Permalink) {
@@ -59,6 +61,14 @@ internal class LoadTimelineStrategy(
                 null
             }
         }
+
+//        fun getRootThreadEventId(): String? {
+//            return if (this is Thread) {
+//                rootThreadEventId
+//            } else {
+//                null
+//            }
+//        }
     }
 
     data class Dependencies(
@@ -71,6 +81,7 @@ internal class LoadTimelineStrategy(
             val timelineInput: TimelineInput,
             val timelineEventMapper: TimelineEventMapper,
             val threadsAwarenessHandler: ThreadsAwarenessHandler,
+            val lightweightSettingsStorage: LightweightSettingsStorage,
             val onEventsUpdated: (Boolean) -> Unit,
             val onLimitedTimeline: () -> Unit,
             val onNewTimelineEvents: (List<String>) -> Unit
@@ -198,12 +209,20 @@ internal class LoadTimelineStrategy(
     }
 
     private fun getChunkEntity(realm: Realm): RealmResults<ChunkEntity> {
-        return if (mode is Mode.Permalink) {
-            ChunkEntity.findAllIncludingEvents(realm, listOf(mode.originEventId))
-        } else {
-            ChunkEntity.where(realm, roomId)
-                    .equalTo(ChunkEntityFields.IS_LAST_FORWARD, true)
-                    .findAll()
+        return when (mode) {
+            is Mode.Live      -> {
+                ChunkEntity.where(realm, roomId)
+                        .equalTo(ChunkEntityFields.IS_LAST_FORWARD, true)
+                        .findAll()
+            }
+            is Mode.Permalink -> {
+                ChunkEntity.findAllIncludingEvents(realm, listOf(mode.originEventId))
+            }
+            is Mode.Thread    -> {
+                ChunkEntity.where(realm, roomId)
+                        .equalTo(ChunkEntityFields.IS_LAST_FORWARD, true)
+                        .findAll()
+            }
         }
     }
 
@@ -224,6 +243,7 @@ internal class LoadTimelineStrategy(
                     timelineEventMapper = dependencies.timelineEventMapper,
                     uiEchoManager = uiEchoManager,
                     threadsAwarenessHandler = dependencies.threadsAwarenessHandler,
+                    lightweightSettingsStorage = dependencies.lightweightSettingsStorage,
                     initialEventId = mode.originEventId(),
                     onBuiltEvents = dependencies.onEventsUpdated
             )
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 6af03a858ad8c885a236789426b4a5747a80fe0c..c0dc31fcf8a988fec4c068ccdd0afc05354cc8d0 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
@@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.events.model.EventType
 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.lightweight.LightweightSettingsStorage
 import org.matrix.android.sdk.internal.database.mapper.EventMapper
 import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
 import org.matrix.android.sdk.internal.database.model.ChunkEntity
@@ -55,6 +56,7 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
                              private val timelineEventMapper: TimelineEventMapper,
                              private val uiEchoManager: UIEchoManager? = null,
                              private val threadsAwarenessHandler: ThreadsAwarenessHandler,
+                             private val lightweightSettingsStorage: LightweightSettingsStorage,
                              private val initialEventId: String?,
                              private val onBuiltEvents: (Boolean) -> Unit) {
 
@@ -88,11 +90,10 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
     private val timelineEventsChangeListener =
             OrderedRealmCollectionChangeListener { results: RealmResults<TimelineEventEntity>, changeSet: OrderedCollectionChangeSet ->
                 Timber.v("on timeline events chunk update")
-                val frozenResults = results.freeze()
-                handleDatabaseChangeSet(frozenResults, changeSet)
+                handleDatabaseChangeSet(results, changeSet)
             }
 
-    private var timelineEventEntities: RealmResults<TimelineEventEntity> = chunkEntity.sortedTimelineEvents()
+    private var timelineEventEntities: RealmResults<TimelineEventEntity> = chunkEntity.sortedTimelineEvents(timelineSettings.rootThreadEventId)
     private val builtEvents: MutableList<TimelineEvent> = Collections.synchronizedList(ArrayList())
     private val builtEventsIndexes: MutableMap<String, Int> = Collections.synchronizedMap(HashMap<String, Int>())
 
@@ -137,13 +138,18 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
         } else if (direction == Timeline.Direction.BACKWARDS && prevChunk != null) {
             return prevChunk?.loadMore(count, direction, fetchOnServerIfNeeded) ?: LoadMoreResult.FAILURE
         }
-        val loadFromStorageCount = loadFromStorage(count, direction)
-        Timber.v("Has loaded $loadFromStorageCount items from storage in $direction")
-        val offsetCount = count - loadFromStorageCount
+        val loadFromStorage = loadFromStorage(count, direction).also {
+            logLoadedFromStorage(it, direction)
+        }
+
+        val offsetCount = count - loadFromStorage.numberOfEvents
+
         return if (direction == Timeline.Direction.FORWARDS && isLastForward.get()) {
             LoadMoreResult.REACHED_END
         } else if (direction == Timeline.Direction.BACKWARDS && isLastBackward.get()) {
             LoadMoreResult.REACHED_END
+        } else if (timelineSettings.isThreadTimeline() && loadFromStorage.threadReachedEnd) {
+            LoadMoreResult.REACHED_END
         } else if (offsetCount == 0) {
             LoadMoreResult.SUCCESS
         } else {
@@ -187,6 +193,16 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
         }
     }
 
+    /**
+     * Simple log that displays the number and timeline of loaded events
+     */
+    private fun logLoadedFromStorage(loadedFromStorage: LoadedFromStorage, direction: Timeline.Direction) {
+        Timber.v("[" +
+                "${if (timelineSettings.isThreadTimeline()) "ThreadTimeLine" else "Timeline"}] Has loaded " +
+                "${loadedFromStorage.numberOfEvents} items from storage in $direction " +
+                if (timelineSettings.isThreadTimeline() && loadedFromStorage.threadReachedEnd) "[Reached End]" else "")
+    }
+
     fun getBuiltEventIndex(eventId: String, searchInNext: Boolean, searchInPrev: Boolean): Int? {
         val builtEventIndex = builtEventsIndexes[eventId]
         if (builtEventIndex != null) {
@@ -267,13 +283,23 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
 
     /**
      * This method tries to read events from the current chunk.
+     * @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): Int {
-        val displayIndex = getNextDisplayIndex(direction) ?: return 0
+    private fun loadFromStorage(count: Int, direction: Timeline.Direction): LoadedFromStorage {
+        val displayIndex = getNextDisplayIndex(direction) ?: return LoadedFromStorage()
         val baseQuery = timelineEventEntities.where()
-        val timelineEvents = baseQuery.offsets(direction, count, displayIndex).findAll().orEmpty()
-        if (timelineEvents.isEmpty()) return 0
-        fetchRootThreadEventsIfNeeded(timelineEvents)
+
+        val timelineEvents = baseQuery
+                .offsets(direction, count, displayIndex)
+                .findAll()
+                .orEmpty()
+
+        if (timelineEvents.isEmpty()) return LoadedFromStorage()
+// Disabled due to the new fallback
+//        if(!lightweightSettingsStorage.areThreadMessagesEnabled()) {
+//            fetchRootThreadEventsIfNeeded(timelineEvents)
+//        }
         if (direction == Timeline.Direction.FORWARDS) {
             builtEventsIndexes.entries.forEach { it.setValue(it.value + timelineEvents.size) }
         }
@@ -291,9 +317,20 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
                         builtEvents.add(timelineEvent)
                     }
                 }
-        return timelineEvents.size
+        return LoadedFromStorage(
+                threadReachedEnd = threadReachedEnd(timelineEvents),
+                numberOfEvents = timelineEvents.size)
     }
 
+    /**
+     * Returns whether or not the the thread has reached end. It returns false if the current timeline
+     * is not a thread timeline
+     */
+    private fun threadReachedEnd(timelineEvents: List<TimelineEventEntity>): Boolean =
+            timelineSettings.rootThreadEventId?.let { rootThreadId ->
+                timelineEvents.firstOrNull { it.eventId == rootThreadId }?.let { true }
+            } ?: false
+
     /**
      * This function is responsible to fetch and store the root event of a thread event
      * in order to be able to display the event to the user appropriately
@@ -316,6 +353,10 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
                 timelineEvent.root.mxDecryptionResult == null) {
             timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineId)) }
         }
+        if (!timelineEvent.isEncrypted() && !lightweightSettingsStorage.areThreadMessagesEnabled()) {
+            // Thread aware for not encrypted events
+            timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineId)) }
+        }
         return timelineEvent
     }
 
@@ -343,7 +384,8 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
         val loadMoreResult = try {
             if (token == null) {
                 if (direction == Timeline.Direction.BACKWARDS || !chunkEntity.hasBeenALastForwardChunk()) return LoadMoreResult.REACHED_END
-                val lastKnownEventId = chunkEntity.sortedTimelineEvents().firstOrNull()?.eventId ?: return LoadMoreResult.FAILURE
+                val lastKnownEventId = chunkEntity.sortedTimelineEvents(timelineSettings.rootThreadEventId).firstOrNull()?.eventId
+                        ?: return LoadMoreResult.FAILURE
                 val taskParams = FetchTokenAndPaginateTask.Params(roomId, lastKnownEventId, direction.toPaginationDirection(), count)
                 fetchTokenAndPaginateTask.execute(taskParams).toLoadMoreResult()
             } else {
@@ -352,7 +394,7 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
                 paginationTask.execute(taskParams).toLoadMoreResult()
             }
         } catch (failure: Throwable) {
-            Timber.e("Failed to fetch from server: $failure", failure)
+            Timber.e(failure, "Failed to fetch from server")
             LoadMoreResult.FAILURE
         }
         return if (loadMoreResult == LoadMoreResult.SUCCESS) {
@@ -385,10 +427,10 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
      * This method is responsible for managing insertions and updates of events on this chunk.
      *
      */
-    private fun handleDatabaseChangeSet(frozenResults: RealmResults<TimelineEventEntity>, changeSet: OrderedCollectionChangeSet) {
+    private fun handleDatabaseChangeSet(results: RealmResults<TimelineEventEntity>, changeSet: OrderedCollectionChangeSet) {
         val insertions = changeSet.insertionRanges
         for (range in insertions) {
-            val newItems = frozenResults
+            val newItems = results
                     .subList(range.startIndex, range.startIndex + range.length)
                     .map { it.buildAndDecryptIfNeeded() }
             builtEventsIndexes.entries.filter { it.value >= range.startIndex }.forEach { it.setValue(it.value + range.length) }
@@ -404,7 +446,7 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
         val modifications = changeSet.changeRanges
         for (range in modifications) {
             for (modificationIndex in (range.startIndex until range.startIndex + range.length)) {
-                val updatedEntity = frozenResults[modificationIndex] ?: continue
+                val updatedEntity = results[modificationIndex] ?: continue
                 try {
                     builtEvents[modificationIndex] = updatedEntity.buildAndDecryptIfNeeded()
                 } catch (failure: Throwable) {
@@ -418,17 +460,16 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
     }
 
     private fun getNextDisplayIndex(direction: Timeline.Direction): Int? {
-        val frozenTimelineEvents = timelineEventEntities.freeze()
-        if (frozenTimelineEvents.isEmpty()) {
+        if (timelineEventEntities.isEmpty()) {
             return null
         }
         return if (builtEvents.isEmpty()) {
             if (initialEventId != null) {
-                frozenTimelineEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, initialEventId).findFirst()?.displayIndex
+                timelineEventEntities.where().equalTo(TimelineEventEntityFields.EVENT_ID, initialEventId).findFirst()?.displayIndex
             } else if (direction == Timeline.Direction.BACKWARDS) {
-                frozenTimelineEvents.first(null)?.displayIndex
+                timelineEventEntities.first(null)?.displayIndex
             } else {
-                frozenTimelineEvents.last(null)?.displayIndex
+                timelineEventEntities.last(null)?.displayIndex
             }
         } else if (direction == Timeline.Direction.FORWARDS) {
             builtEvents.first().displayIndex + 1
@@ -450,10 +491,16 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
                 timelineEventMapper = timelineEventMapper,
                 uiEchoManager = uiEchoManager,
                 threadsAwarenessHandler = threadsAwarenessHandler,
+                lightweightSettingsStorage = lightweightSettingsStorage,
                 initialEventId = null,
                 onBuiltEvents = this.onBuiltEvents
         )
     }
+
+    private data class LoadedFromStorage(
+            val threadReachedEnd: Boolean = false,
+            val numberOfEvents: Int = 0
+    )
 }
 
 private fun RealmQuery<TimelineEventEntity>.offsets(
@@ -474,6 +521,19 @@ private fun Timeline.Direction.toPaginationDirection(): PaginationDirection {
     return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS
 }
 
-private fun ChunkEntity.sortedTimelineEvents(): RealmResults<TimelineEventEntity> {
-    return timelineEvents.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
+private fun ChunkEntity.sortedTimelineEvents(rootThreadEventId: String?): RealmResults<TimelineEventEntity> {
+    return if (rootThreadEventId == null) {
+        timelineEvents
+                .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
+    } else {
+        timelineEvents
+                .where()
+                .beginGroup()
+                .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId)
+                .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/TimelineEventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt
index 75d02dfd98f233b1a4dbb3831ef7e096dbefcc8e..49a8a8b55a33279e3a5abd98c6707b7eaf96d84b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt
@@ -23,6 +23,8 @@ import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.internal.crypto.NewSessionListener
 import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
+import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
+import org.matrix.android.sdk.internal.database.mapper.asDomain
 import org.matrix.android.sdk.internal.database.model.EventEntity
 import org.matrix.android.sdk.internal.database.query.where
 import org.matrix.android.sdk.internal.di.SessionDatabase
@@ -36,7 +38,8 @@ internal class TimelineEventDecryptor @Inject constructor(
         @SessionDatabase
         private val realmConfiguration: RealmConfiguration,
         private val cryptoService: CryptoService,
-        private val threadsAwarenessHandler: ThreadsAwarenessHandler
+        private val threadsAwarenessHandler: ThreadsAwarenessHandler,
+        private val lightweightSettingsStorage: LightweightSettingsStorage
 ) {
 
     private val newSessionListener = object : NewSessionListener {
@@ -101,9 +104,27 @@ internal class TimelineEventDecryptor @Inject constructor(
         }
     }
 
+    private fun threadAwareNonEncryptedEvents(request: DecryptionRequest, realm: Realm) {
+        val event = request.event
+            realm.executeTransaction {
+                val eventId = event.eventId ?: return@executeTransaction
+                val eventEntity = EventEntity
+                        .where(it, eventId = eventId)
+                        .findFirst()
+                val decryptedEvent = eventEntity?.asDomain()
+                threadsAwarenessHandler.makeEventThreadAware(realm, event.roomId, decryptedEvent, eventEntity)
+        }
+    }
     private fun processDecryptRequest(request: DecryptionRequest, realm: Realm) {
         val event = request.event
         val timelineId = request.timelineId
+
+        if (!request.event.isEncrypted()) {
+            // Here we have requested a decryption to an event that is not encrypted
+            // We will simply make this event thread aware
+            threadAwareNonEncryptedEvents(request, realm)
+            return
+        }
         try {
             val result = cryptoService.decryptEvent(request.event, timelineId)
             Timber.v("Successfully decrypted event ${event.eventId}")
@@ -112,15 +133,9 @@ internal class TimelineEventDecryptor @Inject constructor(
                 val eventEntity = EventEntity
                         .where(it, eventId = eventId)
                         .findFirst()
-
-                eventEntity?.apply {
-                    val decryptedPayload = threadsAwarenessHandler.handleIfNeededDuringDecryption(
-                            it,
-                            roomId = event.roomId,
-                            event,
-                            result)
-                    setDecryptionResult(result, decryptedPayload)
-                }
+                eventEntity?.setDecryptionResult(result)
+                val decryptedEvent = eventEntity?.asDomain()
+                threadsAwarenessHandler.makeEventThreadAware(realm, event.roomId, decryptedEvent, eventEntity)
             }
         } catch (e: MXCryptoError) {
             Timber.v("Failed to decrypt event ${event.eventId} : ${e.localizedMessage}")
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 a85f0dbdc93a35f6626feaa6bf8f415d003755e7..6607e71bd9cb530bc96ef00b673b7e602ebaf174 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
@@ -26,8 +26,11 @@ import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.internal.database.helper.addIfNecessary
 import org.matrix.android.sdk.internal.database.helper.addStateEvent
 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.lightweight.LightweightSettingsStorage
 import org.matrix.android.sdk.internal.database.mapper.toEntity
 import org.matrix.android.sdk.internal.database.model.ChunkEntity
+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
@@ -36,6 +39,7 @@ import org.matrix.android.sdk.internal.database.query.create
 import org.matrix.android.sdk.internal.database.query.find
 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.StreamEventsManager
 import org.matrix.android.sdk.internal.util.awaitTransaction
 import timber.log.Timber
@@ -45,8 +49,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,
-        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,
@@ -90,6 +96,7 @@ internal class TokenChunkEventPersistor @Inject constructor(
                         handlePagination(realm, roomId, direction, receivedChunk, currentChunk)
                     }
                 }
+
         return if (receivedChunk.events.isEmpty()) {
             if (receivedChunk.hasMore()) {
                 Result.SHOULD_FETCH_MORE
@@ -132,6 +139,7 @@ internal class TokenChunkEventPersistor @Inject constructor(
                 roomMemberContentsByUser[stateEvent.stateKey] = stateEvent.content.toModel<RoomMemberContent>()
             }
         }
+        val optimizedThreadSummaryMap = hashMapOf<String, EventEntity>()
         run processTimelineEvents@{
             eventList.forEach { event ->
                 if (event.eventId == null || event.senderId == null) {
@@ -176,10 +184,28 @@ internal class TokenChunkEventPersistor @Inject constructor(
                 }
                 liveEventManager.get().dispatchPaginatedEventReceived(event, roomId)
                 currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser)
+                if (lightweightSettingsStorage.areThreadMessagesEnabled()) {
+                    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
+                    }
+                }
             }
         }
         if (currentChunk.isValid) {
             RoomEntity.where(realm, roomId).findFirst()?.addIfNecessary(currentChunk)
         }
+
+        if (lightweightSettingsStorage.areThreadMessagesEnabled()) {
+            optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(
+                    roomId = roomId,
+                    realm = realm,
+                    currentUserId = userId,
+                    chunkEntity = currentChunk
+            )
+        }
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt
index 16d36c0cd92e1a5af8776f5897628628e461ffec..bb926232493733b2f3d5e4203b2d524f2b017c14 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt
@@ -66,11 +66,11 @@ internal class UIEchoManager(private val listener: Listener) {
         return existingState != sendState
     }
 
-    fun onLocalEchoCreated(timelineEvent: TimelineEvent): Boolean  {
+    fun onLocalEchoCreated(timelineEvent: TimelineEvent): Boolean {
         when (timelineEvent.root.getClearType()) {
             EventType.REDACTION -> {
             }
-            EventType.REACTION -> {
+            EventType.REACTION  -> {
                 val content: ReactionContent? = timelineEvent.root.content?.toModel<ReactionContent>()
                 if (RelationType.ANNOTATION == content?.relatesTo?.type) {
                     val reaction = content.relatesTo.key
@@ -104,8 +104,8 @@ internal class UIEchoManager(private val listener: Listener) {
         val updateReactions = existingAnnotationSummary.reactionsSummary.toMutableList()
 
         contents.forEach { uiEchoReaction ->
-            val existing = updateReactions.firstOrNull { it.key == uiEchoReaction.reaction }
-            if (existing == null) {
+            val indexOfExistingReaction = updateReactions.indexOfFirst { it.key == uiEchoReaction.reaction }
+            if (indexOfExistingReaction == -1) {
                 // just add the new key
                 ReactionAggregatedSummary(
                         key = uiEchoReaction.reaction,
@@ -117,6 +117,7 @@ internal class UIEchoManager(private val listener: Listener) {
                 ).let { updateReactions.add(it) }
             } else {
                 // update Existing Key
+                val existing = updateReactions[indexOfExistingReaction]
                 if (!existing.localEchoEvents.contains(uiEchoReaction.localEchoId)) {
                     updateReactions.remove(existing)
                     // only update if echo is not yet there
@@ -128,7 +129,7 @@ internal class UIEchoManager(private val listener: Listener) {
                             sourceEvents = existing.sourceEvents,
                             localEchoEvents = existing.localEchoEvents + uiEchoReaction.localEchoId
 
-                    ).let { updateReactions.add(it) }
+                    ).let { updateReactions.add(indexOfExistingReaction, it) }
                 }
             }
         }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/SearchTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/SearchTask.kt
index 8de762ee1b6ae540356a2c7749b80e7e639bd20a..3ba7d11c3df2b7d7d9eeb235f06385680a8c1297 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/SearchTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/SearchTask.kt
@@ -19,6 +19,10 @@ package org.matrix.android.sdk.internal.session.search
 import org.matrix.android.sdk.api.session.search.EventAndSender
 import org.matrix.android.sdk.api.session.search.SearchResult
 import org.matrix.android.sdk.api.util.MatrixItem
+import org.matrix.android.sdk.internal.database.RealmSessionProvider
+import org.matrix.android.sdk.internal.database.mapper.asDomain
+import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
+import org.matrix.android.sdk.internal.database.query.where
 import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
 import org.matrix.android.sdk.internal.network.executeRequest
 import org.matrix.android.sdk.internal.session.search.request.SearchRequestBody
@@ -28,6 +32,7 @@ import org.matrix.android.sdk.internal.session.search.request.SearchRequestFilte
 import org.matrix.android.sdk.internal.session.search.request.SearchRequestOrder
 import org.matrix.android.sdk.internal.session.search.request.SearchRequestRoomEvents
 import org.matrix.android.sdk.internal.session.search.response.SearchResponse
+import org.matrix.android.sdk.internal.session.search.response.SearchResponseItem
 import org.matrix.android.sdk.internal.task.Task
 import javax.inject.Inject
 
@@ -47,7 +52,8 @@ internal interface SearchTask : Task<SearchTask.Params, SearchResult> {
 
 internal class DefaultSearchTask @Inject constructor(
         private val searchAPI: SearchAPI,
-        private val globalErrorReceiver: GlobalErrorReceiver
+        private val globalErrorReceiver: GlobalErrorReceiver,
+        private val realmSessionProvider: RealmSessionProvider
 ) : SearchTask {
 
     override suspend fun execute(params: SearchTask.Params): SearchResult {
@@ -74,12 +80,22 @@ internal class DefaultSearchTask @Inject constructor(
     }
 
     private fun SearchResponse.toDomain(): SearchResult {
+        val localTimelineEvents = findRootThreadEventsFromDB(searchCategories.roomEvents?.results)
         return SearchResult(
                 nextBatch = searchCategories.roomEvents?.nextBatch,
                 highlights = searchCategories.roomEvents?.highlights,
                 results = searchCategories.roomEvents?.results?.map { searchResponseItem ->
+
+                    val localThreadEventDetails = localTimelineEvents
+                            ?.firstOrNull { it.eventId ==  searchResponseItem.event.eventId }
+                            ?.root
+                            ?.asDomain()
+                            ?.threadDetails
+
                     EventAndSender(
-                            searchResponseItem.event,
+                            searchResponseItem.event.apply {
+                                threadDetails = localThreadEventDetails
+                            },
                             searchResponseItem.event.senderId?.let { senderId ->
                                 searchResponseItem.context?.profileInfo?.get(senderId)
                                         ?.let {
@@ -94,4 +110,19 @@ internal class DefaultSearchTask @Inject constructor(
                 }?.reversed()
         )
     }
+
+    /**
+     * Find local events if exists in order to enhance the result with thread summary
+     */
+    private fun findRootThreadEventsFromDB(searchResponseItemList: List<SearchResponseItem>?): List<TimelineEventEntity>? {
+        return realmSessionProvider.withRealm { realm ->
+            searchResponseItemList?.mapNotNull {
+                it.event.roomId ?: return@mapNotNull null
+                it.event.eventId ?: return@mapNotNull null
+                TimelineEventEntity.where(realm, it.event.roomId, it.event.eventId).findFirst()
+            }?.filter {
+                it.root?.isRootThread == true || it.root?.isThread() == true
+            }
+        }
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpace.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpace.kt
index 8589db27b1579ffa3f2644e8432aa4528afa8c38..303eda49d89d216e94a4faab02fd5add6e53ee00 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpace.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpace.kt
@@ -40,10 +40,6 @@ internal class DefaultSpace(
 
     override val spaceId = room.roomId
 
-    override suspend fun leave(reason: String?) {
-        return room.leave(reason)
-    }
-
     override fun spaceSummary(): RoomSummary? {
         return spaceSummaryDataSource.getSpaceSummary(room.roomId)
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt
index ebd5f2578ef539331333f3c941b6d429b7e99e46..c18055e0894f9f8f3e44ac64d2175347cd7a47b3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt
@@ -184,6 +184,10 @@ internal class DefaultSpaceService @Inject constructor(
         return joinSpaceTask.execute(JoinSpaceTask.Params(spaceIdOrAlias, reason, viaServers))
     }
 
+    override suspend fun leaveSpace(spaceId: String, reason: String?) {
+        leaveRoomTask.execute(LeaveRoomTask.Params(spaceId, reason))
+    }
+
     override suspend fun rejectInvite(spaceId: String, reason: String?) {
         leaveRoomTask.execute(LeaveRoomTask.Params(spaceId, reason))
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt
index f178074507c37e6be2509a1341da3e860ff5029c..f93da9705d0d58b91c2c893b150bb351f1b250b5 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt
@@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.session.sync.model.RoomsSyncResponse
 import org.matrix.android.sdk.api.session.sync.model.SyncResponse
 import org.matrix.android.sdk.internal.SessionManager
 import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
+import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
 import org.matrix.android.sdk.internal.di.SessionDatabase
 import org.matrix.android.sdk.internal.di.SessionId
 import org.matrix.android.sdk.internal.di.WorkManagerProvider
@@ -64,6 +65,7 @@ internal class SyncResponseHandler @Inject constructor(
         private val aggregatorHandler: SyncResponsePostTreatmentAggregatorHandler,
         private val cryptoService: DefaultCryptoService,
         private val tokenStore: SyncTokenStore,
+        private val lightweightSettingsStorage: LightweightSettingsStorage,
         private val processEventForPushTask: ProcessEventForPushTask,
         private val pushRuleService: PushRuleService,
         private val threadsAwarenessHandler: ThreadsAwarenessHandler,
@@ -101,7 +103,10 @@ internal class SyncResponseHandler @Inject constructor(
         val aggregator = SyncResponsePostTreatmentAggregator()
 
         // Prerequisite for thread events handling in RoomSyncHandler
-        threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(syncResponse)
+// Disabled due to the new fallback
+//        if (!lightweightSettingsStorage.areThreadMessagesEnabled()) {
+//            threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(syncResponse)
+//        }
 
         // Start one big transaction
         monarchy.awaitTransaction { realm ->
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 170e8b734bbc96a900b434fe40536e6f55ea4bd3..99e6521eb70b76656b477d0cedc15055b98d9d6d 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
@@ -36,10 +36,13 @@ 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.updateThreadSummaryIfNeeded
+import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
 import org.matrix.android.sdk.internal.database.mapper.asDomain
 import org.matrix.android.sdk.internal.database.mapper.toEntity
 import org.matrix.android.sdk.internal.database.model.ChunkEntity
 import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
+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
@@ -81,6 +84,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                                                    private val threadsAwarenessHandler: ThreadsAwarenessHandler,
                                                    private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
                                                    @UserId private val userId: String,
+                                                   private val lightweightSettingsStorage: LightweightSettingsStorage,
                                                    private val timelineInput: TimelineInput,
                                                    private val liveEventService: Lazy<StreamEventsManager>) {
 
@@ -363,10 +367,12 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
         val eventIds = ArrayList<String>(eventList.size)
         val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
 
+        val optimizedThreadSummaryMap = hashMapOf<String, EventEntity>()
         for (event in eventList) {
             if (event.eventId == null || event.senderId == null || event.type == null) {
                 continue
             }
+
             eventIds.add(event.eventId)
             liveEventService.get().dispatchLiveEventReceived(event, roomId, insertType == EventInsertType.INITIAL_SYNC)
 
@@ -375,14 +381,13 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
             if (event.isEncrypted() && !isInitialSync) {
                 decryptIfNeeded(event, roomId)
             }
-
-            threadsAwarenessHandler.handleIfNeeded(
-                    realm = realm,
-                    roomId = roomId,
-                    event = event)
+            var contentToInject: String? = null
+            if (!isInitialSync) {
+                contentToInject = threadsAwarenessHandler.makeEventThreadAware(realm, roomId, event)
+            }
 
             val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it }
-            val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType)
+            val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs, contentToInject).copyToRealmOrIgnore(realm, insertType)
             if (event.stateKey != null) {
                 CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
                     eventId = event.eventId
@@ -402,6 +407,15 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
             }
 
             chunkEntity.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser)
+            if (lightweightSettingsStorage.areThreadMessagesEnabled()) {
+                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
+                }
+            }
             // Give info to crypto module
             cryptoService.onLiveEvent(roomEntity.roomId, event)
 
@@ -426,9 +440,15 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                 }
             }
         }
-
         // Handle deletion of [stuck] local echos if needed
-         deleteLocalEchosIfNeeded(insertType, roomEntity, eventList)
+        deleteLocalEchosIfNeeded(insertType, roomEntity, eventList)
+        if (lightweightSettingsStorage.areThreadMessagesEnabled()) {
+            optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(
+                    roomId = roomId,
+                    realm = realm,
+                    chunkEntity = chunkEntity,
+                    currentUserId = userId)
+        }
 
         // posting new events to timeline if any is registered
         timelineInput.onNewTimelineEvents(roomId = roomId, eventIds = eventIds)
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 767a967522e93b9833f5249f57eacf6e4ed75909..f3a1523955383185046dcae80ec8d93d7f635440 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
@@ -18,26 +18,35 @@ package org.matrix.android.sdk.internal.session.sync.handler.room
 
 import com.zhuinden.monarchy.Monarchy
 import io.realm.Realm
-import org.matrix.android.sdk.api.session.crypto.CryptoService
-import org.matrix.android.sdk.api.session.crypto.MXCryptoError
+import io.realm.kotlin.where
+import org.matrix.android.sdk.api.session.events.model.Content
 import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.RelationType
+import org.matrix.android.sdk.api.session.events.model.getRelationContentForType
+import org.matrix.android.sdk.api.session.events.model.getRootThreadEventId
+import org.matrix.android.sdk.api.session.events.model.isSticker
 import org.matrix.android.sdk.api.session.events.model.toContent
 import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.session.room.model.message.MessageFormat
 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.send.SendState
 import org.matrix.android.sdk.api.session.sync.model.SyncResponse
 import org.matrix.android.sdk.api.util.JsonDict
-import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult
 import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
+import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
+import org.matrix.android.sdk.internal.database.mapper.ContentMapper
 import org.matrix.android.sdk.internal.database.mapper.EventMapper
+import org.matrix.android.sdk.internal.database.mapper.asDomain
 import org.matrix.android.sdk.internal.database.mapper.toEntity
 import org.matrix.android.sdk.internal.database.model.EventEntity
+import org.matrix.android.sdk.internal.database.model.EventEntityFields
 import org.matrix.android.sdk.internal.database.model.EventInsertType
 import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
 import org.matrix.android.sdk.internal.database.query.where
+import org.matrix.android.sdk.internal.di.MoshiProvider
 import org.matrix.android.sdk.internal.di.SessionDatabase
 import org.matrix.android.sdk.internal.session.permalinks.PermalinkFactory
 import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
@@ -52,11 +61,16 @@ import javax.inject.Inject
  */
 internal class ThreadsAwarenessHandler @Inject constructor(
         private val permalinkFactory: PermalinkFactory,
-        private val cryptoService: CryptoService,
         @SessionDatabase private val monarchy: Monarchy,
+        private val lightweightSettingsStorage: LightweightSettingsStorage,
         private val getEventTask: GetEventTask
 ) {
 
+    // This caching is responsible to improve the performance when we receive a root event
+    // to be able to know this event is a root one without checking the DB,
+    // We update the list with all thread root events by checking if there is a m.thread relation on the events
+    private val cacheEventRootId = hashSetOf<String>()
+
     /**
      * Fetch root thread events if they are missing from the local storage
      * @param syncResponse the sync response
@@ -84,7 +98,7 @@ internal class ThreadsAwarenessHandler @Inject constructor(
         if (eventList.isNullOrEmpty()) return
 
         val threadsToFetch = emptyMap<String, String>().toMutableMap()
-        Realm.getInstance(monarchy.realmConfiguration).use {  realm ->
+        Realm.getInstance(monarchy.realmConfiguration).use { realm ->
             eventList.asSequence()
                     .filter {
                         isThreadEvent(it) && it.roomId != null
@@ -139,96 +153,186 @@ internal class ThreadsAwarenessHandler @Inject constructor(
 
     /**
      * Handle events mainly coming from the RoomSyncHandler
+     * @return The content to inject in the roomSyncHandler live events
      */
-    fun handleIfNeeded(realm: Realm,
-                       roomId: String,
-                       event: Event) {
-        val payload = transformThreadToReplyIfNeeded(
-                realm = realm,
-                roomId = roomId,
-                event = event,
-                decryptedResult = event.mxDecryptionResult?.payload) ?: return
+    fun makeEventThreadAware(realm: Realm,
+                             roomId: String?,
+                             event: Event?,
+                             eventEntity: EventEntity? = null): String? {
+        event ?: return null
+        roomId ?: return null
+        if (lightweightSettingsStorage.areThreadMessagesEnabled()) return null
+        handleRootThreadEventsIfNeeded(realm, roomId, eventEntity, event)
+        if (!isThreadEvent(event)) return null
+        val eventPayload = if (!event.isEncrypted()) {
+            event.content?.toMutableMap() ?: return null
+        } else {
+            event.mxDecryptionResult?.payload?.toMutableMap() ?: return null
+        }
+        val eventBody = event.getDecryptedTextSummary() ?: return null
+        val eventIdToInject = getPreviousEventOrRoot(event) ?: run {
+            return@makeEventThreadAware injectFallbackIndicator(event, eventBody, eventEntity, eventPayload)
+        }
+        val eventToInject = getEventFromDB(realm, eventIdToInject)
+        val eventToInjectBody = eventToInject?.getDecryptedTextSummary()
+        var contentForNonEncrypted: String?
+        if (eventToInject != null && eventToInjectBody != null) {
+            // If the event to inject exists and is decrypted
+            // Inject it to our event
+            val messageTextContent = injectEvent(
+                    roomId = roomId,
+                    eventBody = eventBody,
+                    eventToInject = eventToInject,
+                    eventToInjectBody = eventToInjectBody) ?: return null
+            // update the event
+            contentForNonEncrypted = updateEventEntity(event, eventEntity, eventPayload, messageTextContent)
+        } else {
+            contentForNonEncrypted = injectFallbackIndicator(event, eventBody, eventEntity, eventPayload)
+        }
 
-        event.mxDecryptionResult = event.mxDecryptionResult?.copy(payload = payload)
+        // Now lets try to find relations for improved results, while some events may come with reverse order
+        eventEntity?.let {
+            // When eventEntity is not null means that we are not from within roomSyncHandler
+            handleEventsThatRelatesTo(realm, roomId, event, eventBody, false)
+        }
+        return contentForNonEncrypted
     }
 
     /**
-     * Handle events while they are being decrypted
+     * Handle for not thread events that we have marked them as root.
+     * Find relations and inject them accordingly
+     * @param eventEntity the current eventEntity received
+     * @param event the current event received
+     * @return The content to inject in the roomSyncHandler live events
      */
-    fun handleIfNeededDuringDecryption(realm: Realm,
-                                       roomId: String?,
-                                       event: Event,
-                                       result: MXEventDecryptionResult): JsonDict? {
-        return transformThreadToReplyIfNeeded(
-                realm = realm,
-                roomId = roomId,
-                event = event,
-                decryptedResult = result.clearEvent)
+    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 null
     }
 
     /**
-     * If the event is a thread event then transform/enhance it to a visual Reply Event,
-     * If the event is not a thread event, null value will be returned
-     * If there is an error (ex. the root/origin thread event is not found), null willl be returend
+     * This function is responsible to check if there is any event that relates to our current event
+     * This is useful when we receive an event that relates to a missing parent, so when later we receive the parent
+     * we can update the child as well
+     * @param event the current event that we examine
+     * @param eventBody the current body of the event
+     * @param isFromCache determines whether or not we already know this is root thread event
+     * @return The content to inject in the roomSyncHandler live events
      */
-    private fun transformThreadToReplyIfNeeded(realm: Realm, roomId: String?, event: Event, decryptedResult: JsonDict?): JsonDict? {
-        roomId ?: return null
-        if (!isThreadEvent(event)) return null
-        val rootThreadEventId = getRootThreadEventId(event) ?: return null
-        val payload = decryptedResult?.toMutableMap() ?: return null
-        val body = getValueFromPayload(payload, "body") ?: return null
-        val msgType = getValueFromPayload(payload, "msgtype") ?: return null
-        val rootThreadEvent = getEventFromDB(realm, rootThreadEventId) ?: return null
-        val rootThreadEventSenderId = rootThreadEvent.senderId ?: return null
+    private fun handleEventsThatRelatesTo(realm: Realm, roomId: String, event: Event, eventBody: String, isFromCache: Boolean): String? {
+        event.eventId ?: return null
+        val rootThreadEventId = if (isFromCache) event.eventId else event.getRootThreadEventId() ?: return null
+        eventThatRelatesTo(realm, event.eventId, rootThreadEventId)?.forEach { eventEntityFound ->
+            val newEventFound = eventEntityFound.asDomain()
+            val newEventBody = newEventFound.getDecryptedTextSummary() ?: return null
+            val newEventPayload = newEventFound.mxDecryptionResult?.payload?.toMutableMap() ?: return null
 
-        decryptIfNeeded(rootThreadEvent, roomId)
+            val messageTextContent = injectEvent(
+                    roomId = roomId,
+                    eventBody = newEventBody,
+                    eventToInject = event,
+                    eventToInjectBody = eventBody) ?: return null
+
+            return updateEventEntity(newEventFound, eventEntityFound, newEventPayload, messageTextContent)
+        }
+        return null
+    }
 
-        val rootThreadEventBody = getValueFromPayload(rootThreadEvent.mxDecryptionResult?.payload?.toMutableMap(), "body")
+    /**
+     * Actual update the eventEntity with the new payload
+     * @return the content to inject when this is executed by RoomSyncHandler
+     */
+    private fun updateEventEntity(event: Event,
+                                  eventEntity: EventEntity?,
+                                  eventPayload: MutableMap<String, Any>,
+                                  messageTextContent: Content): String? {
+        eventPayload["content"] = messageTextContent
 
-        val permalink = permalinkFactory.createPermalink(roomId, rootThreadEventId, false)
-        val userLink = permalinkFactory.createPermalink(rootThreadEventSenderId, false) ?: ""
+        if (event.isEncrypted()) {
+            if (event.isSticker()) {
+                eventPayload["type"] = EventType.MESSAGE
+            }
+            event.mxDecryptionResult = event.mxDecryptionResult?.copy(payload = eventPayload)
+            eventEntity?.decryptionResultJson = event.mxDecryptionResult?.let {
+                MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).toJson(it)
+            }
+        } else {
+            if (event.type == EventType.STICKER) {
+                eventEntity?.type = EventType.MESSAGE
+            }
+            eventEntity?.content = ContentMapper.map(messageTextContent)
+            return ContentMapper.map(messageTextContent)
+        }
+        return null
+    }
 
+    /**
+     * Injecting $eventToInject decrypted content as a reply to $event
+     * @param eventToInject the event that will inject
+     * @param eventBody the actual event body
+     * @return The final content with the injected event
+     */
+    private fun injectEvent(roomId: String,
+                            eventBody: String,
+                            eventToInject: Event,
+                            eventToInjectBody: String): Content? {
+        val eventToInjectId = eventToInject.eventId ?: return null
+        val eventIdToInjectSenderId = eventToInject.senderId.orEmpty()
+        val permalink = permalinkFactory.createPermalink(roomId, eventToInjectId, false)
+        val userLink = permalinkFactory.createPermalink(eventIdToInjectSenderId, false) ?: ""
         val replyFormatted = LocalEchoEventFactory.REPLY_PATTERN.format(
                 permalink,
                 userLink,
-                rootThreadEventSenderId,
-                // Remove inner mx_reply tags if any
-                rootThreadEventBody,
-                body)
+                eventIdToInjectSenderId,
+                eventToInjectBody,
+                eventBody)
 
-        val messageTextContent = MessageTextContent(
-                msgType = msgType,
+        return MessageTextContent(
+                msgType = MessageType.MSGTYPE_TEXT,
                 format = MessageFormat.FORMAT_MATRIX_HTML,
-                body = body,
+                body = eventBody,
                 formattedBody = replyFormatted
         ).toContent()
-
-        payload["content"] = messageTextContent
-
-        return payload
     }
 
     /**
-     * Decrypt the event
+     * Integrate fallback Quote reply
      */
+    private fun injectFallbackIndicator(event: Event,
+                                        eventBody: String,
+                                        eventEntity: EventEntity?,
+                                        eventPayload: MutableMap<String, Any>): String? {
+        val replyFormatted = LocalEchoEventFactory.QUOTE_PATTERN.format(
+                "In reply to a thread",
+                eventBody)
 
-    private fun decryptIfNeeded(event: Event, roomId: String) {
-        try {
-            if (!event.isEncrypted() || event.mxDecryptionResult != null) return
+        val messageTextContent = MessageTextContent(
+                msgType = MessageType.MSGTYPE_TEXT,
+                format = MessageFormat.FORMAT_MATRIX_HTML,
+                body = eventBody,
+                formattedBody = replyFormatted
+        ).toContent()
 
-            // Event from sync does not have roomId, so add it to the event first
-            val result = cryptoService.decryptEvent(event.copy(roomId = roomId), "")
-            event.mxDecryptionResult = OlmDecryptionResult(
-                    payload = result.clearEvent,
-                    senderKey = result.senderCurve25519Key,
-                    keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
-                    forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
-            )
-        } catch (e: MXCryptoError) {
-            if (e is MXCryptoError.Base) {
-                event.mCryptoError = e.errorType
-                event.mCryptoErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription
-            }
+        return updateEventEntity(event, eventEntity, eventPayload, messageTextContent)
+    }
+
+    private fun eventThatRelatesTo(realm: Realm, currentEventId: String, rootThreadEventId: String): List<EventEntity>? {
+        val threadList = realm.where<EventEntity>()
+                .beginGroup()
+                .equalTo(EventEntityFields.ROOT_THREAD_EVENT_ID, rootThreadEventId)
+                .or()
+                .equalTo(EventEntityFields.EVENT_ID, rootThreadEventId)
+                .endGroup()
+                .and()
+                .findAll()
+        cacheEventRootId.add(rootThreadEventId)
+        return threadList.filter {
+            it.asDomain().getRelationContentForType(RelationType.IO_THREAD)?.inReplyTo?.eventId == currentEventId
         }
     }
 
@@ -246,7 +350,7 @@ internal class ThreadsAwarenessHandler @Inject constructor(
      * @param event
      */
     private fun isThreadEvent(event: Event): Boolean =
-            event.content.toModel<MessageRelationContent>()?.relatesTo?.type == RelationType.THREAD
+            event.content.toModel<MessageRelationContent>()?.relatesTo?.type == RelationType.IO_THREAD
 
     /**
      * Returns the root thread eventId or null otherwise
@@ -255,6 +359,9 @@ internal class ThreadsAwarenessHandler @Inject constructor(
     private fun getRootThreadEventId(event: Event): String? =
             event.content.toModel<MessageRelationContent>()?.relatesTo?.eventId
 
+    private fun getPreviousEventOrRoot(event: Event): String? =
+            event.content.toModel<MessageRelationContent>()?.relatesTo?.inReplyTo?.eventId
+
     @Suppress("UNCHECKED_CAST")
     private fun getValueFromPayload(payload: JsonDict?, key: String): String? {
         val content = payload?.get("content") as? JsonDict
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt
index 0ecf91f6fae6137fd13ba99015e729de8b9b959a..97ae9b3a689ed8c84a9b3977a4ff8df86a8d7751 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt
@@ -192,12 +192,14 @@ abstract class SyncService : Service() {
         }
     }
 
+    abstract fun provideMatrix(): Matrix
+
     private fun initialize(intent: Intent?): Boolean {
         if (intent == null) {
             Timber.d("## Sync: initialize intent is null")
             return false
         }
-        val matrix = Matrix.getInstance(applicationContext)
+        val matrix = provideMatrix()
         val safeSessionId = intent.getStringExtra(EXTRA_SESSION_ID) ?: return false
         syncTimeoutSeconds = intent.getIntExtra(EXTRA_TIMEOUT_SECONDS, getDefaultSyncTimeoutSeconds())
         syncDelaySeconds = intent.getIntExtra(EXTRA_DELAY_SECONDS, getDefaultSyncDelaySeconds())
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/database/RealmMigrator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/database/RealmMigrator.kt
new file mode 100644
index 0000000000000000000000000000000000000000..15e82f3cc0f570c62c607106a6c021d66d8da143
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/database/RealmMigrator.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.util.database
+
+import io.realm.DynamicRealm
+import io.realm.RealmObjectSchema
+import timber.log.Timber
+
+abstract class RealmMigrator(private val realm: DynamicRealm,
+                             private val targetSchemaVersion: Int) {
+    fun perform() {
+        Timber.d("Migrate ${realm.configuration.realmFileName} to $targetSchemaVersion")
+        doMigrate(realm)
+    }
+
+    abstract fun doMigrate(realm: DynamicRealm)
+
+    protected fun RealmObjectSchema.addFieldIfNotExists(fieldName: String, fieldType: Class<*>): RealmObjectSchema {
+        if (!hasField(fieldName)) {
+            addField(fieldName, fieldType)
+        }
+        return this
+    }
+
+    protected fun RealmObjectSchema.removeFieldIfExists(fieldName: String): RealmObjectSchema {
+        if (hasField(fieldName)) {
+            removeField(fieldName)
+        }
+        return this
+    }
+
+    protected fun RealmObjectSchema.setRequiredIfNotAlready(fieldName: String, isRequired: Boolean): RealmObjectSchema {
+        if (isRequired != isRequired(fieldName)) {
+            setRequired(fieldName, isRequired)
+        }
+        return this
+    }
+}